Wed, 03 Jul 2024 20:01:31 +0200
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)); }