main/task_http.c

Wed, 03 Jul 2024 20:01:31 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Wed, 03 Jul 2024 20:01:31 +0200
branch
idf 5.1
changeset 142
1f7069278fe7
parent 137
e0f50087c909
permissions
-rw-r--r--

Version 0.4.2. Removed the components/websocket server and switched to the official http and websockets server. This server will also recover if the wifi connection disconnects and reconnects.

/**
 * @file task_http.c
 * @brief HTTP and Websocket server functions.
 */
#include "config.h"
#include "sys/param.h"
#include "mbedtls/base64.h"
#include "mbedtls/sha1.h"
#include "cJSON.h"


static const char		*TAG = "task_http";
httpd_handle_t			web_server = NULL;			///< The http server handle.

cJSON				*root = NULL;
cJSON				*touch = NULL;


typedef struct _ls_list {
    struct _ls_list		*next;
    char			d_name[64];
    off_t			size;
    long			mtime;
} ls_list;


/**
 * @brief Clear the linked list and release memory.
 */
void tidy_lslist(ls_list **lap)
{
    ls_list	*tmp, *old;

    for (tmp = *lap; tmp; tmp = old) {
	old = tmp->next;
	free(tmp);
    }
    *lap = NULL;
}


/**
 * @brief Add a file entry to the linked list.
 * @param lap Pointer to the linked list.
 * @param name The entry filename.
 * @param size The entry filesize.
 * @param mtime The entry filedate.
 */
void fill_list(ls_list **lap, char *name, off_t size, long mtime)
{
    ls_list	**tmp;

    for (tmp = lap; *tmp; tmp = &((*tmp)->next));

    *tmp = (ls_list *)malloc(sizeof(ls_list));
    (*tmp)->next = NULL;
    strncpy((*tmp)->d_name, name, 63);
    (*tmp)->size = size;
    (*tmp)->mtime = mtime;
    tmp = &((*tmp)->next);
}


/**
 * @brief Compare for sorting files by datetime, reversed.
 * @param lsp1 Entry 1
 * @param lsp2 Entry 2
 */
int comp(ls_list **lsp1, ls_list **lsp2)
{
    char	as[20], bs[20];

    snprintf(as, 19, "%ld", (*lsp1)->mtime);
    snprintf(bs, 19, "%ld", (*lsp2)->mtime);
    // Reverse order
    return strcmp(bs, as);
}


/**
 * @brief Sort the linked list.
 */
void sort_list(ls_list **lap)
{
    ls_list	*ta, **vector;
    size_t	n = 0, i;

    if (*lap == NULL)
	return;

    for (ta = *lap; ta; ta = ta->next)
	n++;
    vector = (ls_list **)malloc(n * sizeof(ls_list *));
    i = 0;

    for (ta = *lap; ta; ta = ta->next) {
	vector[i++] = ta;
    }
    qsort(vector, n, sizeof(ls_list *), (int(*)(const void*, const void*))comp);

    (*lap) = vector[0];
    i = 1;

    for (ta = *lap; ta; ta = ta->next) {
	if (i < n)
	    ta->next = vector[i++];
	else
	    ta->next = NULL;
    }

    free(vector);
    return;
}


#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)

/**
 * @brief Set HTTP response content type according to file extension
 * @param req The request where the content type will be set.
 * @param filepath The filename to get the extension from.
 * @return ESP_OK if success.
 */
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath)
{
    const char *type = "text/plain";
    if (CHECK_FILE_EXTENSION(filepath, ".html")) {
        type = "text/html";
    } else if (CHECK_FILE_EXTENSION(filepath, ".log")) {
	type = "text/plain";
    } else if (CHECK_FILE_EXTENSION(filepath, ".js")) {
        type = "application/javascript";
    } else if (CHECK_FILE_EXTENSION(filepath, ".json")) {
	type = "text/json";
    } else if (CHECK_FILE_EXTENSION(filepath, ".gz")) {
	type = "application/x-gzip";
    } else if (CHECK_FILE_EXTENSION(filepath, ".css")) {
        type = "text/css";
    } else if (CHECK_FILE_EXTENSION(filepath, ".png")) {
        type = "image/png";
    } else if (CHECK_FILE_EXTENSION(filepath, ".ico")) {
        type = "image/x-icon";
    } else if (CHECK_FILE_EXTENSION(filepath, ".svg")) {
        type = "text/xml";
    } else if (CHECK_FILE_EXTENSION(filepath, ".ttf")) {
	type = "font/ttf";
    } else if (CHECK_FILE_EXTENSION(filepath, ".woff")) {
	type = "font/woff";
    } else if (CHECK_FILE_EXTENSION(filepath, ".woff2")) {
        type = "font/woff2";
    } else if (CHECK_FILE_EXTENSION(filepath, ".xml")) {
	type = "text/xml";
    }
    return httpd_resp_set_type(req, type);
}


/**
 * @brief Send text message to a single websocket client.
 * @param num The client socket to send to.
 * @param msg The message to send.
 * @return ESP_OK.
 */
int ws_server_send_text_client(int num, char* msg)
{
    httpd_ws_frame_t    ws_pkt;

    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;
    ws_pkt.len = strlen(msg);
    ws_pkt.payload = (uint8_t*)msg;
    httpd_ws_send_frame_async(web_server, num, &ws_pkt);
    ESP_LOGD(TAG, "ws_server_send_text_client(%d, %s)", num, msg);
    return ESP_OK;
}


/**
 * @brief Broadcast text message to all connected websocket clients.
 * @param msg The text message.
 * @return -1 if error, else the number of clients.
 */
int ws_server_send_text_clients(char* msg)
{
    httpd_ws_frame_t	ws_pkt;
    static size_t	max_clients = CONFIG_LWIP_MAX_LISTENING_TCP;
    size_t		fds = max_clients;
    int			client_fds[CONFIG_LWIP_MAX_LISTENING_TCP] = {0};
    int			count = 0;

    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;
    ws_pkt.len = strlen(msg);
    ws_pkt.payload = (uint8_t*)msg;

    esp_err_t ret = httpd_get_client_list(web_server, &fds, client_fds);
    if (ret != ESP_OK) {
        return -1;
    }

    for (int i = 0; i < fds; i++) {
        httpd_ws_client_info_t client_info = httpd_ws_get_fd_info(web_server, client_fds[i]);
        if (client_info == HTTPD_WS_CLIENT_WEBSOCKET) {
            httpd_ws_send_frame_async(web_server, client_fds[i], &ws_pkt);
            count++;
        }
    }
    return count;
}


/**
 * @brief Websocket handler.
 * @param req The request structure.
 * @return ESP_OK or error.
 */
static esp_err_t ws_handler(httpd_req_t *req)
{
    if (req->method == HTTP_GET) {
        ESP_LOGI(TAG, "New websocket connection %d", httpd_req_to_sockfd(req));
	/* Send initial screen */
	TFTstartWS(httpd_req_to_sockfd(req));
        return ESP_OK;
    }

    httpd_ws_frame_t	ws_pkt;
    uint8_t		*buf = NULL;
    char		jbuf[128];
    cJSON		*root = NULL, *touch = NULL;

    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;
    /* Set max_len = 0 to get the frame len */
    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "ws_handler() httpd_ws_recv_frame failed to get frame len with %d", ret);
        return ret;
    }
    ESP_LOGD(TAG, "frame len is %d type is %d socket %d", ws_pkt.len, ws_pkt.type, httpd_req_to_sockfd(req));

    if (ws_pkt.len) {
        /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */
        buf = calloc(1, ws_pkt.len + 1);
        if (buf == NULL) {
            ESP_LOGE(TAG, "ws_handler() no memory for buf");
            return ESP_ERR_NO_MEM;
        }
        ws_pkt.payload = buf;
        /* Set max_len = ws_pkt.len to get the frame payload */
        ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "ws_handler() httpd_ws_recv_frame failed with %d", ret);
            free(buf);
            return ret;
        }
        ESP_LOGI(TAG, "ws_handler() Got message: %s", ws_pkt.payload);
    }

    if ((ws_pkt.type == HTTPD_WS_TYPE_TEXT) && (ws_pkt.len > 0) && (ws_pkt.len < 128)) {
        memcpy(jbuf, ws_pkt.payload, ws_pkt.len);
        jbuf[ws_pkt.len] = '\0';
        if ((root = cJSON_Parse(jbuf))) {
            if ((touch = cJSON_GetObjectItem(root,"touch"))) {
                int x = cJSON_GetObjectItem(touch, "x")->valueint;
                int y = cJSON_GetObjectItem(touch, "y")->valueint;
		WS_touched(x, y);
            } else {
                ESP_LOGI(TAG,"not json touch");
            }
            cJSON_Delete(root);
        } else {
            ESP_LOGI(TAG,"not json");
        }
    }

    free(buf);
    return ESP_OK;
}


/**
 * @brief Handle files download from /spiffs or /sdcard.
 */
static esp_err_t files_handler(httpd_req_t *req)
{
    char	temp_url[560], temp_url_gz[564];
    struct stat	st;
    off_t	filesize;
    char	strftime_buf[64];
    bool	send_gz;
    int		fd = -1;

    if (req->uri[strlen(req->uri) - 1] == '/') {
        // If no filename given, use index.html
        sprintf(temp_url, "/spiffs/w%sindex.html", req->uri);
    } else if (strncmp(req->uri, "/log/", 5) == 0) {
        // Logfiles are on the SD card.
        sprintf(temp_url, "/sdcard/w%s", req->uri);
    } else {
        sprintf(temp_url, "/spiffs/w%s", req->uri);
        for (int i = 0; i < 127; i++) {
            if (temp_url[i] == '?')
                temp_url[i] = '\0';
            if (temp_url[i] == '\0')
                break;
        }
    }
    snprintf(temp_url_gz, 563, "%s.gz", temp_url);

    /*
     * Get filesize and date for the response headers.
     * Note that the /spiffs filesystem doesn't support file timestamps
     * and returns the epoch for each file. Therefore we cannot use
     * the cache based on timestamps.
     */
    filesize = 0;
    strftime_buf[0] = '\0';
    send_gz = false;
    if (stat(temp_url_gz, &st) == 0) {
	/* The requested file is gzipped. */
	filesize = st.st_size;
	strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime)));
	send_gz = true;
    } else if (stat(temp_url, &st) == 0) {
	filesize = st.st_size;
	strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime)));
    }
    ESP_LOGI(TAG, "GET `%s` file %s date %s size %d", req->uri, (send_gz) ? temp_url_gz : temp_url, strftime_buf, (int)filesize);

    if (send_gz) {
	fd = open(temp_url_gz, O_RDONLY, 0);
    } else {
	fd = open(temp_url, O_RDONLY, 0);
    }
    if (fd == -1) {
	ESP_LOGI(TAG, "files_handler() file not found");
	httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
	return ESP_FAIL;
    }

    set_content_type_from_file(req, temp_url);
    if (send_gz) {
	httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
    }

    /*
     * Now the real file send in chunks.
     */
    char *chunk = malloc(1024);
    ssize_t read_bytes;
    do {
        read_bytes = read(fd, chunk, 1024);
        if (read_bytes == -1) {
            ESP_LOGE(TAG, "files_handler() Failed to read file : %s", (send_gz) ? temp_url_gz : temp_url);
        } else if (read_bytes > 0) {
            /* Send the buffer contents as HTTP response chunk */
            if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) {
                close(fd);
                ESP_LOGE(TAG, "files_handler() 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;
            }
        }
    } while (read_bytes > 0);
    /* Close file after sending complete */
    close(fd);

    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}


/**
 * @brief Handle 'GET /logfiles.json'
 */
static esp_err_t logfiles_handler(httpd_req_t *req)
{
    ls_list     *lsx = NULL, *tmp;
    char        temp[64];
    FILE        *dest = fopen("/spiffs/w/logfiles.json", "w");

    if (dest) {
        DIR *dir = opendir("/sdcard/w/log");
        if (dir) {
            struct dirent* de = readdir(dir);
            struct stat st;
            while (de) {
                snprintf(temp, 63, "/sdcard/w/log/");
                strncat(temp, de->d_name, 63 - strlen(temp));
                if (stat(temp, &st) == ESP_OK) {
                    fill_list(&lsx, de->d_name, st.st_size, st.st_mtime);
                }
                de = readdir(dir);
                vTaskDelay(5 / portTICK_PERIOD_MS);
            }
            closedir(dir);
        } else {
            ESP_LOGE(TAG, "Error %d open directory /sdcard/w/log", errno);
        }

        sort_list(&lsx);
        bool comma = false;
        fprintf(dest, "{\"Dir\":[{\"Folder\":\"/log\",\"Files\":[");
        for (tmp = lsx; tmp; tmp = tmp->next) {
            fprintf(dest, "%s{\"File\":\"%s\",\"Size\":%ld,\"Date\":%ld}", (comma)?",":"", tmp->d_name, tmp->size, tmp->mtime);
            comma = true;
        }
        fprintf(dest, "]}]}");
        fclose(dest);
        tidy_lslist(&lsx);
    } else {
        ESP_LOGE(TAG, "Error %d write /spiffs/w/logfiles.json", errno);
	httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file");
        return ESP_FAIL;
    }

    /*
     * File is ready, send it using the files_handler().
     */
    esp_err_t ret = files_handler(req);
    unlink("/spiffs/w/logfiles.json");
    return ret;
}


esp_err_t start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    config.uri_match_fn = httpd_uri_match_wildcard;

    // Start the httpd server
    if (web_server == NULL) {
	ESP_LOGI(TAG, "Starting httpd on port: '%d'", config.server_port);
    	if (httpd_start(&web_server, &config) == ESP_OK) {
            // Set URI handlers
            ESP_LOGD(TAG, "Registering URI handlers");

	    httpd_uri_t ws = {
		.uri        = "/ws",
		.method     = HTTP_GET,
		.handler    = ws_handler,
		.user_ctx   = NULL,
		.is_websocket = true
	    };
	    httpd_register_uri_handler(web_server, &ws);

	    httpd_uri_t logfiles = {
		.uri = "/logfiles.json",
		.method = HTTP_GET,
		.handler = logfiles_handler,
		.user_ctx   = NULL
	    };
	    httpd_register_uri_handler(web_server, &logfiles);

	    httpd_uri_t vfs_files = {
            	.uri = "/*",
            	.method = HTTP_GET,
            	.handler = files_handler,
            	.user_ctx = NULL
	    };
	    httpd_register_uri_handler(web_server, &vfs_files);
            return ESP_OK;
	}
    } else {
	ESP_LOGI(TAG, "httpd server already started");
	return ESP_OK;
    }

    ESP_LOGI(TAG, "Error starting server!");
    return ESP_FAIL;
}


static esp_err_t stop_webserver(void)
{
    esp_err_t	ret;

    ESP_LOGI(TAG, "Stopping httpd server");
    ret = httpd_stop(web_server);
    web_server = NULL;
    return ret;
}


static void disconnect_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    if (web_server) {
        if (stop_webserver() == ESP_OK) {
            web_server = NULL;
        } else {
            ESP_LOGE(TAG, "Failed to stop http server");
        }
    }
}


static void connect_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
    start_webserver();
}


/**
 * @brief Install the HTTP server event handlers.
 *        The real server is started and stopped on events.
 */
void install_http_server(void)
{
    ESP_LOGI(TAG, "Install HTTP server");

    /*
     * Respond to WiFi and network events. This is much better then letting the
     * server run forever.
     */
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, web_server));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_LOST_IP, &disconnect_handler, web_server));
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, web_server));
}

mercurial