main/task_http.c

Sat, 20 Oct 2018 13:23:15 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 20 Oct 2018 13:23:15 +0200
changeset 0
b74b0e4902c3
child 1
ad2c8b13eb88
permissions
-rw-r--r--

Initial checkin brewboard

/**
 * @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"


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;



void dump_buf(char *buf, uint16_t buflen);


/**
 * @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[128], header[128], 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;
	}
    }
    sprintf(temp_url_gz, "%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, "Not found", "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(".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) {
	sprintf(header, "%sContent-Encoding: gzip\r\n", header);
    }
    sprintf(header, "%s\r\n", header);	// Add last empty line.
    err = netconn_write(conn, header, strlen(header), NETCONN_NOCOPY);
    if (err != ERR_OK) {
	ESP_LOGE(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_LOGE(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 VNC websocket events.
 */
void websockify_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len)
{
    switch(type) {
	case WEBSOCKET_CONNECT:
			ESP_LOGI(TAG,"Websockify client %i connected!",num);
			break;

	case WEBSOCKET_DISCONNECT_EXTERNAL:
			ESP_LOGI(TAG,"Websockify client %i sent a disconnect message",num);
			VncStopWS(num);
			break;

	case WEBSOCKET_DISCONNECT_INTERNAL:
			ESP_LOGI(TAG,"Websockify client %i was disconnected",num);
			VncStopWS(num);
			break;

	case WEBSOCKET_DISCONNECT_ERROR:
			ESP_LOGI(TAG,"Websockify client %i was disconnected due to an error",num);
			VncStopWS(num);
			break;
	
	case WEBSOCKET_TEXT:
			ESP_LOGI(TAG,"Websockiify client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg);
			break;

	case WEBSOCKET_BIN:
			VncGetWSmessage(msg, len);
			//dump_buf(msg, 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;
    }
}



/**
 * @brief Handle web ui websocket events.
 */
void websock_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len)
{
    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:
			ESP_LOGI(TAG,"Websocket client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg);
			dump_buf(msg, len);
			break;

	case WEBSOCKET_BIN:
			ESP_LOGI(TAG,"Websocket client %i sent bin message of size %i:\n",num,(uint32_t)len);
			dump_buf(msg, 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;
    }
}



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");
}



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

    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_LOGI(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) {
	    	fprintf(dest, "{\"Dir\":[{\"Folder\":\"/log\",\"Files\":[");
	    	DIR *dir = opendir("/sdcard/w/log");
	    	if (dir) {
		    struct dirent* de = readdir(dir);
		    struct stat st;
		    bool comma = false;
		    while (de) {
		    	sprintf(temp, "/sdcard/w/log/%s", de->d_name);
		    	if (stat(temp, &st) == ESP_OK) {
			    fprintf(dest, "%s{\"File\":\"%s\",\"Size\":%ld,\"Date\":%ld}", (comma)?",":"", de->d_name, st.st_size, st.st_mtime);
			    comma = true;
		    	}
		    	de = readdir(dir);
		    	vTaskDelay(5 / portTICK_PERIOD_MS);
		    }
		    closedir(dir);
	    	}
	    	fprintf(dest, "]}]}");
	    	fclose(dest);
	    }
	    http_sendfile(conn, "/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 noVNC.
	if ((strstr(buf,"GET /websockify ") && strstr(buf,"Upgrade: websocket"))) {
	    int nr = ws_server_add_client_protocol(conn, buf, buflen, "/websockify", "binary", websockify_callback);
	    ESP_LOGI(TAG, "%s new websocket on /websockify client: %d", ipstr, nr);
	    netbuf_delete(inbuf);
	    VncStartWS(nr);
	    return;
	}

	// websocket for web UI.
	if ((strstr(buf,"GET /ws ") && strstr(buf,"Upgrade: websocket"))) {
	    int nr = ws_server_add_client_protocol(conn, buf, buflen, "/ws", "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;
	}

	dump_buf(buf, buflen);

	if (strstr(buf, "GET /")) {
		ESP_LOGI(TAG, "%s request: %s", ipstr, buf);
		http_error(conn, 404, "Not found", "Not found");
	} else {
		http_error(conn, 405, "Invalid method", "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_LOGE(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