Mon, 19 Feb 2024 15:07:28 +0100
Fix current year display
/** * @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 "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; 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; void tidy_lslist(ls_list **lap) { ls_list *tmp, *old; for (tmp = *lap; tmp; tmp = old) { old = tmp->next; free(tmp); } *lap = NULL; } 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); } 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); } 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; } /** * @brief Debug dump buffer * @param buf The buffer * @param buflen Length of the buffer */ #if 0 void dump_buf(char *buf, uint16_t buflen); #endif /** * @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. */ static void http_error(struct netconn *conn, int error, char *message, char *body) { char *tmp = malloc(200); 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); } netconn_write(conn, tmp, strlen(tmp), NETCONN_NOCOPY); free(tmp); } /** * @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. */ static void http_sendfile(struct netconn *conn, char *url, char *mod_since, char *ipstr) { char temp_url[128], temp_url_gz[132], header[256], c_type[32]; struct stat st; off_t filesize; size_t sentsize; char strftime_buf[64]; err_t err; bool send_gz; FILE *f; 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); } 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; } } snprintf(temp_url_gz, 131, "%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) { 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))); } /* * 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 if (send_gz) { f = fopen(temp_url_gz, "r"); } else { f = fopen(temp_url, "r"); } 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 (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); } 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); } } /** * @brief Handle web ui websocket events. */ void websock_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len) { char jbuf[128]; switch(type) { case WEBSOCKET_CONNECT: case WEBSOCKET_DISCONNECT_EXTERNAL: break; case WEBSOCKET_DISCONNECT_INTERNAL: ESP_LOGI(TAG,"Websocket client %i was disconnected",num); break; case WEBSOCKET_DISCONNECT_ERROR: ESP_LOGI(TAG,"Websocket client %i was disconnected due to an error",num); break; 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,(uint32_t)len,msg); break; case WEBSOCKET_BIN: ESP_LOGI(TAG,"Websocket client %i sent bin message of size %i:\n",num,(uint32_t)len); break; case WEBSOCKET_PING: ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(uint32_t)len,msg); break; case WEBSOCKET_PONG: ESP_LOGI(TAG,"client %i responded to the ping",num); break; } } #if 0 void dump_buf(char *buf, uint16_t buflen) { 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"); } #endif /** * @brief Serve any client. * @param http_client_sock The socket on which the client is connected. */ static void http_serve(struct netconn *conn) { 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; } } 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); } /** * @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); }