main/task_http.c

Mon, 21 Jun 2021 19:04:10 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Mon, 21 Jun 2021 19:04:10 +0200
changeset 102
96e30a3a3980
parent 94
87aa80b8e452
permissions
-rw-r--r--

Finished experimental code to drive the German HendiControl board. Added BoilPower and RampPower buttons during the while boil process. RampPower (going to boil power) is now adjustable. Added PWM driver code to the driver task.

/**
 * @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);
}

mercurial