--- a/main/task_http.c Mon Jul 01 08:38:57 2024 +0200 +++ b/main/task_http.c Wed Jul 03 20:01:31 2024 +0200 @@ -1,23 +1,21 @@ /** * @file task_http.c * @brief HTTP and Websocket server functions. - * This uses some ESP32 Websocket code written by Blake Felt - blake.w.felt@gmail.com */ #include "config.h" +#include "sys/param.h" #include "mbedtls/base64.h" #include "mbedtls/sha1.h" #include "cJSON.h" static const char *TAG = "task_http"; -static QueueHandle_t client_queue; -const static int client_queue_size = 10; -static TaskHandle_t xTaskHTTP = NULL; -static TaskHandle_t xTaskQueue = NULL; +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]; @@ -26,7 +24,9 @@ } ls_list; - +/** + * @brief Clear the linked list and release memory. + */ void tidy_lslist(ls_list **lap) { ls_list *tmp, *old; @@ -39,7 +39,13 @@ } - +/** + * @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; @@ -55,7 +61,11 @@ } - +/** + * @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]; @@ -67,7 +77,9 @@ } - +/** + * @brief Sort the linked list. + */ void sort_list(ls_list **lap) { ls_list *ta, **vector; @@ -101,74 +113,199 @@ } +#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0) /** - * @brief Debug dump buffer - * @param buf The buffer - * @param buflen Length of the buffer + * @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. */ -#if 0 -void dump_buf(char *buf, uint16_t buflen); -#endif +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 HTTP error and message - * @param conn The socket to send to. - * @param error The HTTP error code. - * @param message Yhe human readable explanation. - * @paeam body The human readable explanation. + * @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. */ -static void http_error(struct netconn *conn, int error, char *message, char *body) +int ws_server_send_text_client(int num, char* msg) { - char *tmp = malloc(200); + 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; +} + - ESP_LOGI(TAG, "Http response %d - %s", error, message); - if (strlen(body)) { - snprintf(tmp, 199, "HTTP/1.1 %d %s\r\nContent-type: text/plain\r\n\r\n%s\n", error, message, body); - } else { - snprintf(tmp, 199, "HTTP/1.1 %d %s\r\n\r\n", error, message); +/** + * @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; } - netconn_write(conn, tmp, strlen(tmp), NETCONN_NOCOPY); - free(tmp); + + 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 Send HTTP file from the filesystem. - * @param conn The network connection. - * @param url The requested url. - * @param mod_since The date/time of the file, or NULL. - * @param ipstr The IP address of the remote. + * @brief Handle files download from /spiffs or /sdcard. */ -static void http_sendfile(struct netconn *conn, char *url, char *mod_since, char *ipstr) +static esp_err_t files_handler(httpd_req_t *req) { - char temp_url[128], temp_url_gz[132], header[256], c_type[32]; + char temp_url[560], temp_url_gz[564]; struct stat st; off_t filesize; - size_t sentsize; char strftime_buf[64]; - err_t err; bool send_gz; - FILE *f; + int fd = -1; - if (url[strlen(url) - 1] == '/') { - // If no filename given, use index.html - sprintf(temp_url, "/spiffs/w%sindex.html", url); - } else if (strncmp(url, "/log/", 5) == 0) { - // Logfiles are on the SD card. - sprintf(temp_url, "/sdcard/w%s", url); + 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", url); - for (int i = 0; i < 127; i++) { - if (temp_url[i] == '?') - temp_url[i] = '\0'; - if (temp_url[i] == '\0') - break; - } + 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, 131, "%s.gz", temp_url); + snprintf(temp_url_gz, 563, "%s.gz", temp_url); /* * Get filesize and date for the response headers. @@ -180,6 +317,7 @@ 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; @@ -187,428 +325,201 @@ filesize = st.st_size; strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime))); } - - /* - * If we have a If-Modified-Since parameter, compare that with the current - * filedate on disk. If It's the same send a 304 response. - * Cannot work on /spiffs. - * Update 18-05-2021 it seems better on idf 4.2.1 but still not correct. - */ -#if 0 - time_t Now; - struct tm timeInfo; - if (mod_since && strlen(strftime_buf)) { - time(&Now); - localtime_r(&Now, &timeInfo); - strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", &timeInfo); - sprintf(header, "HTTP/1.1 304 Not Modified\r\nDate: %s\r\n\r\n", strftime_buf); - netconn_write(conn, header, strlen(header), NETCONN_NOCOPY); - ESP_LOGI(TAG, "%s sendfile %s Not Modified, ok", ipstr, temp_url); - return; - } -#endif + 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) { - f = fopen(temp_url_gz, "r"); + fd = open(temp_url_gz, O_RDONLY, 0); } else { - f = fopen(temp_url, "r"); + fd = open(temp_url, O_RDONLY, 0); } - if (f == NULL) { - ESP_LOGI(TAG, "%s url \'%s\' file \'%s\' not found", ipstr, url, temp_url); - http_error(conn, 404, (char *)"Not found", (char *)"Not found"); - return; + 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"); } - if (strcmp(".html", &temp_url[strlen(temp_url) - 5]) == 0) { - sprintf(c_type, "text/html"); - } else if (strcmp(".css", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "text/css"); - } else if (strcmp(".log", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "text/plain"); - } else if (strcmp(".js", &temp_url[strlen(temp_url) - 3]) == 0) { - sprintf(c_type, "text/javascript"); - } else if (strcmp(".json", &temp_url[strlen(temp_url) - 5]) == 0) { - sprintf(c_type, "text/json"); - } else if (strcmp(".gz", &temp_url[strlen(temp_url) - 3]) == 0) { - sprintf(c_type, "application/x-gzip"); - } else if (strcmp(".png", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "image/png"); - } else if (strcmp(".svg", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "image/svg+xml"); - } else if (strcmp(".oga", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "audio/ogg"); - } else if (strcmp(".ico", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "image/x-icon"); - } else if (strcmp(".ttf", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "font/ttf"); - } else if (strcmp(".woff", &temp_url[strlen(temp_url) - 5]) == 0) { - sprintf(c_type, "font/woff"); - } else if (strcmp(".woff2", &temp_url[strlen(temp_url) - 6]) == 0) { - sprintf(c_type, "font/woff2"); - } else if (strcmp(".xml", &temp_url[strlen(temp_url) - 4]) == 0) { - sprintf(c_type, "text/xml"); - } else { - sprintf(c_type, "application/octet-stream"); - printf("Unknown content type for %s\n", temp_url); - } + /* + * 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); - vTaskDelay(2 / portTICK_PERIOD_MS); - // httpdHeader(connData, "Cache-Control", "max-age=3600, must-revalidate"); - if (filesize) { - sprintf(header, "HTTP/1.1 200 OK\r\nLast-Modified: %s\r\nContent-Length: %ld\r\nContent-type: %s\r\n", strftime_buf, filesize, c_type); - } else { - sprintf(header, "HTTP/1.1 200 OK\r\nContent-type: %s\r\n", c_type); - } - if (send_gz) { - strncat(header, "Content-Encoding: gzip\r\n", 255 - strlen(header)); - } - strncat(header, "\r\n", 255 - strlen(header)); // Add last empty line. - err = netconn_write(conn, header, strlen(header), NETCONN_NOCOPY); - if (err != ERR_OK) { - ESP_LOGW(TAG, "%s sendfile %s%s err=%d on header write", ipstr, temp_url, (send_gz) ? ".gz":"", err); - fclose(f); - return; - } - // if (strstr(acceptEncodingBuffer, "gzip") == NULL) - // http_error(conn, 501, "Not implemented", "Your browser does not accept gzip-compressed data."); - - sentsize = 0; - uint8_t *buff = malloc(1024); - size_t bytes; - int pause = 0; - - for (;;) { - bytes = fread(buff, 1, 1024, f); - if (bytes == 0) - break; - - err = netconn_write(conn, buff, bytes, NETCONN_NOCOPY); - if (err != ERR_OK) { - ESP_LOGW(TAG, "%s sendfile %s%s err=%d send %u bytes of %ld bytes", ipstr, temp_url, (send_gz) ? ".gz":"", err, sentsize, filesize); - break; - } - vTaskDelay(2 / portTICK_PERIOD_MS); - sentsize += bytes; - pause++; - if (pause > 50) { // 50 K - pause = 0; - vTaskDelay(50 / portTICK_PERIOD_MS); - } - } - fclose(f); - free(buff); - - if (sentsize == filesize) { - ESP_LOGI(TAG, "%s sendfile %s%s sent %u bytes, ok (%s)", ipstr, temp_url, (send_gz) ? ".gz":"", sentsize, url); - } + /* Respond with an empty chunk to signal HTTP response completion */ + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; } - /** - * @brief Handle web ui websocket events. + * @brief Handle 'GET /logfiles.json' */ -void websock_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len) +static esp_err_t logfiles_handler(httpd_req_t *req) { - char jbuf[128]; + 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); + } - switch(type) { - case WEBSOCKET_CONNECT: - case WEBSOCKET_DISCONNECT_EXTERNAL: - break; + 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; + } - case WEBSOCKET_DISCONNECT_INTERNAL: - ESP_LOGI(TAG,"Websocket client %i was disconnected",num); - break; + /* + * File is ready, send it using the files_handler(). + */ + esp_err_t ret = files_handler(req); + unlink("/spiffs/w/logfiles.json"); + return ret; +} - case WEBSOCKET_DISCONNECT_ERROR: - ESP_LOGI(TAG,"Websocket client %i was disconnected due to an error",num); - break; + +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; - case WEBSOCKET_TEXT: - /* - * Handle json actions from the web clients, like button presses. - */ - if (len < 128) { // Safety, messages are small. - memcpy(jbuf, msg, len); - jbuf[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); - break; - } else { - ESP_LOGI(TAG,"not json touch"); - } - cJSON_Delete(root); - } else { - ESP_LOGI(TAG,"not json"); - } - } - // Log if the message in not processed. - ESP_LOGI(TAG,"Websocket client %i sent text message of size %i:\n%s",num,(unsigned)len,msg); - break; + // 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); - case WEBSOCKET_BIN: - ESP_LOGI(TAG,"Websocket client %i sent bin message of size %i:\n",num,(unsigned)len); - break; + 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; +} + - case WEBSOCKET_PING: - ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(unsigned)len,msg); - break; +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; +} - case WEBSOCKET_PONG: - ESP_LOGI(TAG,"client %i responded to the ping",num); - break; + +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"); + } } } - -#if 0 -void dump_buf(char *buf, uint16_t buflen) +static void connect_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { - int i = 0, l = 1; - - printf("request length %d\n00: ", buflen); - for (;;) { - if (i >= buflen) - break; - if ((buf[i] < ' ') || (buf[i] > 126)) { - if (buf[i] == '\n') { - printf("\\n\n%02d: ", l); - l++; - } else if (buf[i] == '\r') { - printf("\\r"); - } else { - printf("\\%02x", buf[i]); - } - } else { - printf("%c", buf[i]); - } - i++; - } - printf("\n"); + start_webserver(); } -#endif - /** - * @brief Serve any client. - * @param http_client_sock The socket on which the client is connected. + * @brief Install the HTTP server event handlers. + * The real server is started and stopped on events. */ -static void http_serve(struct netconn *conn) +void install_http_server(void) { - char ipstr[IPADDR_STRLEN_MAX]; - struct netbuf *inbuf; - static char *buf; - static uint16_t buflen; - static err_t err; - char url[128], *p, *mod_since = NULL; - ip_addr_t remote_addr; - uint16_t remote_port; - ls_list *lsx = NULL, *tmp; - - if (netconn_getaddr(conn, &remote_addr, &remote_port, 0) == ERR_OK) { - strcpy(ipstr, ip4addr_ntoa(ip_2_ip4(&remote_addr))); - } else { - ipstr[0] = '\0'; - } - - netconn_set_recvtimeout(conn,1000); // allow a connection timeout of 1 second - err = netconn_recv(conn, &inbuf); - - if (err != ERR_OK) { - if (err != ERR_TIMEOUT) { // Ignore timeout - ESP_LOGW(TAG,"%s error %d on read", ipstr, err); - } - netconn_close(conn); - netconn_delete(conn); - netbuf_delete(inbuf); - return; - } - - netbuf_data(inbuf, (void**)&buf, &buflen); - - if (buf) { - /* - * Build url string from the data buffer. - * It looks like: GET /app/localization.js HTTP/1.1 - */ - for (int i = 0; i < 10; i++) { - if (buf[i] == ' ') { - i++; - for (int j = i; j < 128; j++) { - url[j-i] = buf[j]; - if (url[j-i] == ' ') { - url[j-i] = '\0'; - break; - } - } - break; - } - } + ESP_LOGI(TAG, "Install HTTP server"); - if (strstr(buf, "GET /logfiles.json")) { - 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); - } - http_sendfile(conn, (char *)"/logfiles.json", NULL, ipstr); - netconn_close(conn); - netconn_delete(conn); - netbuf_delete(inbuf); - unlink("/spiffs/w/logfiles.json"); - return; - } - - // http requests - if (! strstr(buf,"Upgrade: websocket")) { // Not websocket - p = strstr(buf, "If-Modified-Since:"); // May be cached - if (p) { - size_t mod_len = strcspn(p, " "); - p += (int)(mod_len + 1); - mod_len = strcspn(p, "\r\n"); - mod_since = malloc(mod_len + 2); - memcpy(mod_since, p, mod_len); - mod_since[mod_len] = '\0'; - } - http_sendfile(conn, url, mod_since, ipstr); - if (mod_since) - free(mod_since); - mod_since = NULL; - netconn_close(conn); - netconn_delete(conn); - netbuf_delete(inbuf); - return; - } - - // websocket for web UI. - if ((strstr(buf,"GET /ws ") && strstr(buf,"Upgrade: websocket"))) { - int nr = ws_server_add_client_protocol(conn, buf, buflen, (char *)"/ws", (char *)"binary", websock_callback); - ESP_LOGI(TAG, "%s new websocket on /ws client: %d", ipstr, nr); - netbuf_delete(inbuf); - TFTstartWS(nr); - // Startup something? Init webscreen? - return; - } - -#if 0 - dump_buf(buf, buflen); -#endif - - if (strstr(buf, "GET /")) { - ESP_LOGI(TAG, "%s request: %s", ipstr, buf); - http_error(conn, 404, (char *)"Not found", (char *)"Not found"); - } else { - http_error(conn, 405, (char *)"Invalid method", (char *)"Invalid method"); - } - } - - netconn_close(conn); - netconn_delete(conn); - netbuf_delete(inbuf); + /* + * 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)); } - -/** - * @brief Handles clients when they first connect. passes to a queue - */ -static void task_HTTPserver(void* pvParameters) -{ - struct netconn *conn, *newconn; - static err_t err; - - ESP_LOGI(TAG, "Start HTTP/web server"); - - conn = netconn_new(NETCONN_TCP); - netconn_bind(conn,NULL,80); - netconn_listen(conn); - - do { - err = netconn_accept(conn, &newconn); - if (err == ERR_OK) { - if (xQueueSendToBack(client_queue,&newconn,portMAX_DELAY) != pdTRUE) { - ESP_LOGW(TAG, "xQueueSendToBack() queue full"); - }; - } - vTaskDelay(5 / portTICK_PERIOD_MS); - } while (err == ERR_OK); - - ESP_LOGE(TAG, "Stopping HTTP/web server"); - netconn_close(conn); - netconn_delete(conn); - vTaskDelete(NULL); -} - - - -/** - * @brief Receives clients from queue and handle them. - */ -static void task_Queue(void* pvParameters) -{ - struct netconn* conn; - - ESP_LOGI(TAG, "Start Queue task"); - for(;;) { - xQueueReceive(client_queue, &(conn), portMAX_DELAY); - if (!conn) - continue; - http_serve(conn); - vTaskDelay(2 / portTICK_PERIOD_MS); - } - ESP_LOGE(TAG, "Stopping Queue task"); - vTaskDelete(NULL); -} - - - -void start_http_websocket(void) -{ - ESP_LOGI(TAG, "Start HTTP/Websocket server"); - - client_queue = xQueueCreate(client_queue_size,sizeof(struct netconn*)); - if (client_queue == 0) { - ESP_LOGE(TAG, "Failed to create client_queue"); - return; - } - - ws_server_start(); - xTaskCreate(&task_HTTPserver, "HTTPserver", 5000, NULL, 9, &xTaskHTTP); - xTaskCreate(&task_Queue, "Queue", 6000, NULL, 6, &xTaskQueue); -} -