main/task_http.c

Sat, 06 Jun 2020 22:51:44 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 06 Jun 2020 22:51:44 +0200
changeset 80
9d2c0a85ee6e
parent 64
326c38d3681b
child 87
47253f294a9f
permissions
-rw-r--r--

Added directory sorting for the logfiles. Sort in reverse order on the timestamp so the last files will be on top.

/**
 * @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.
     */
#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, "http_sendfile %s Not Modified, ok", 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(".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:
			ESP_LOGI(TAG,"Websocket client %i connected!",num);
			break;
										            
	case WEBSOCKET_DISCONNECT_EXTERNAL:
			ESP_LOGI(TAG,"Websocket client %i sent a disconnect message",num);
			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, "Starting http server_task");
    client_queue = xQueueCreate(client_queue_size,sizeof(struct netconn*));

    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 server_task");
    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, "Starting 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, "Starting HTTP/Websocket server");

    ws_server_start();
    xTaskCreate(&task_HTTPserver, "HTTPserver", 3000, NULL, 9, &xTaskHTTP);
    xTaskCreate(&task_Queue,      "Queue",      4000, NULL, 6, &xTaskQueue);
}

mercurial