add webserver components, some webserver fices and improvements in templating, add real time history chart

This commit is contained in:
2020-01-07 00:21:27 +01:00
parent ed53611e0f
commit ef7866a29f
50 changed files with 3594 additions and 2 deletions
+106
View File
@@ -0,0 +1,106 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/param.h>
#include "httpd_utils/captive.h"
#include "httpd_utils/fd_to_ipv4.h"
#include "httpd_utils/redirect.h"
#include <common_utils/utils.h>
static const char *TAG="captive";
const char * __attribute__((weak))
httpd_captive_redirect_get_domain(void)
{
return "fb_node.captive";
}
esp_err_t __attribute__((weak))
httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen)
{
buf = append(buf, "http://", &maxlen);
buf = append(buf, httpd_captive_redirect_get_domain(), &maxlen);
append(buf, "/", &maxlen);
return ESP_OK;
}
esp_err_t httpd_captive_redirect(httpd_req_t *r)
{
// must be static to survive being used in the redirect header
static char s_buf[64];
wifi_mode_t mode = 0;
esp_wifi_get_mode(&mode);
// Check if we have an softap interface. No point checking IPs and hostnames if the client can't be on AP.
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_NULL) {
goto no_redirect;
}
int fd = httpd_req_to_sockfd(r);
tcpip_adapter_ip_info_t apip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &apip);
u32_t client_addr;
if(ESP_OK != fd_to_ipv4(fd, &client_addr)) {
return ESP_FAIL;
}
ESP_LOGD(TAG, "[captive] Client addr = 0x%08x, ap addr 0x%08x, ap nmask 0x%08x",
client_addr,
apip.ip.addr,
apip.netmask.addr
);
// Check if client IP looks like from our AP dhcps
if ((client_addr & apip.netmask.addr) != (apip.ip.addr & apip.netmask.addr)) {
ESP_LOGD(TAG, "[captive] Client not in AP IP range");
goto no_redirect;
}
// Get requested hostname from the header
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Host", s_buf, 64);
if (rv != ESP_OK) {
ESP_LOGW(TAG, "[captive] No host in request?");
goto no_redirect;
}
ESP_LOGD(TAG, "[captive] Candidate for redirect: %s%s", s_buf, r->uri);
// Never redirect if host is an IP
if (strlen(s_buf)>7) {
bool isIP = 1;
for (int x = 0; x < strlen(s_buf); x++) {
if (s_buf[x] != '.' && (s_buf[x] < '0' || s_buf[x] > '9')) {
isIP = 0;
break;
}
}
if (isIP) {
ESP_LOGD(TAG, "[captive] Access via IP, no redirect needed");
goto no_redirect;
}
}
// Redirect if host differs
// - this can be e.g. connectivitycheck.gstatic.com or the equivalent for ios
if (0 != strcmp(s_buf, httpd_captive_redirect_get_domain())) {
ESP_LOGD(TAG, "[captive] Host differs, redirecting...");
httpd_captive_redirect_get_url(r, s_buf, 64);
return httpd_redirect_to(r, s_buf);
} else {
ESP_LOGD(TAG, "[captive] Host is OK");
goto no_redirect;
}
no_redirect:
return ESP_ERR_NOT_FOUND;
}
+42
View File
@@ -0,0 +1,42 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include "httpd_utils/fd_to_ipv4.h"
static const char *TAG = "fd2ipv4";
/**
* Get IP address for a FD
*
* @param fd
* @param[out] ipv4
* @return success
*/
esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4)
{
struct sockaddr_in6 addr;
size_t len = sizeof(addr);
int rv = getpeername(fd, (struct sockaddr *) &addr, &len);
if (rv != 0) {
ESP_LOGE(TAG, "Failed to get IP addr for fd %d", fd);
return ESP_FAIL;
}
uint32_t client_addr = 0;
if (addr.sin6_family == AF_INET6) {
// this would fail in a real ipv6 network
// with ipv4 the addr is simply in the last ipv6 byte
struct sockaddr_in6 *s = &addr;
client_addr = s->sin6_addr.un.u32_addr[3];
}
else {
struct sockaddr_in *s = (struct sockaddr_in *) &addr;
client_addr = s->sin_addr.s_addr;
}
*ipv4 = client_addr;
return ESP_OK;
}
+20
View File
@@ -0,0 +1,20 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include "httpd_utils/redirect.h"
static const char *TAG="redirect";
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri)
{
ESP_LOGD(TAG, "Redirect to %s", uri);
httpd_resp_set_hdr(r, "Location", uri);
httpd_resp_set_status(r, "303 See Other");
httpd_resp_set_type(r, HTTPD_TYPE_TEXT);
const char *msg = "Redirect";
return httpd_resp_send(r, msg, -1);
}
+181
View File
@@ -0,0 +1,181 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <rom/queue.h>
#include <malloc.h>
#include <assert.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#include "httpd_utils/session_kvmap.h"
static const char *TAG = "sess_kvmap";
// this struct is opaque, a stub like this is sufficient for the head pointer.
struct sess_kv_entry;
/** Session head structure, dynamically allocated */
SLIST_HEAD(sess_kv_map, sess_kv_entry);
struct sess_kv_entry {
SLIST_ENTRY(sess_kv_entry) link;
char key[SESS_KVMAP_KEY_LEN];
void *value;
sess_kv_free_func_t free_fn;
};
struct sess_kv_map *sess_kv_map_alloc(void)
{
ESP_LOGD(TAG, "kv store alloc");
struct sess_kv_map *map = malloc(sizeof(struct sess_kv_map));
assert(map);
SLIST_INIT(map);
return map;
}
void sess_kv_map_free(void *head_v)
{
struct sess_kv_map* head = head_v;
ESP_LOGD(TAG, "kv store free");
assert(head);
struct sess_kv_entry *item, *tmp;
SLIST_FOREACH_SAFE(item, head, link, tmp) {
if (item->free_fn) {
item->free_fn(item->value);
free(item);
}
}
free(head);
}
void * sess_kv_map_get(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store get %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
ESP_LOGD(TAG, "got ok");
return item->value;
}
}
ESP_LOGD(TAG, "not found in store");
return NULL;
}
void * sess_kv_map_take(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store take %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
item->key[0] = 0;
item->free_fn = NULL;
ESP_LOGD(TAG, "taken ok");
return item->value;
}
}
ESP_LOGD(TAG, "not found in store");
return NULL;
}
esp_err_t sess_kv_map_remove(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store remove %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
if (item->free_fn) {
item->free_fn(item->value);
}
item->key[0] = 0;
item->value = NULL;
item->free_fn = NULL;
return ESP_OK;
}
}
ESP_LOGD(TAG, "couldn't remove, not found: %s", key);
return ESP_ERR_NOT_FOUND;
}
esp_err_t sess_kv_map_set(struct sess_kv_map *head, const char *key, void *value, sess_kv_free_func_t free_fn)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv set value for key %s", key);
size_t key_len = strlen(key);
if (key_len > SESS_KVMAP_KEY_LEN-1) {
ESP_LOGE(TAG, "Key too long: %s", key);
// discard illegal key
return ESP_FAIL;
}
if (key_len == 0) {
ESP_LOGE(TAG, "Key too short: \"%s\"", key);
// discard illegal key
return ESP_FAIL;
}
struct sess_kv_entry *item = NULL;
struct sess_kv_entry *empty_item = NULL; // found item with no content
SLIST_FOREACH(item, head, link) {
ESP_LOGD(TAG, "test item with key %s, ptr %p > %p", item->key, item, item->link.sle_next);
if (0 == item->key[0]) {
ESP_LOGD(TAG, "found an empty slot");
empty_item = item;
}
else if (0==strcmp(item->key, key)) {
ESP_LOGD(TAG, "old value replaced");
if (item->free_fn) {
item->free_fn(item->value);
}
item->value = value;
item->free_fn = free_fn;
return ESP_OK;
} else {
ESP_LOGD(TAG, "skip this one");
}
}
struct sess_kv_entry *new_item = NULL;
// insert new or reuse an empty item
if (empty_item) {
new_item = empty_item;
ESP_LOGD(TAG, "empty item reused (%p)", new_item);
} else {
ESP_LOGD(TAG, "alloc new item");
// key not found, add a new entry.
new_item = malloc(sizeof(struct sess_kv_entry));
if (!new_item) {
ESP_LOGE(TAG, "New entry alloc failed");
return ESP_ERR_NO_MEM;
}
}
strcpy(new_item->key, key);
new_item->free_fn = free_fn;
new_item->value = value;
if (!empty_item) {
ESP_LOGD(TAG, "insert new item into list");
// this item was malloc'd
SLIST_INSERT_HEAD(head, new_item, link);
}
return ESP_OK;
}
+220
View File
@@ -0,0 +1,220 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <malloc.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <esp_http_server.h>
#include <rom/queue.h>
#include "httpd_utils/session_store.h"
// TODO add a limit on simultaneously open sessions (can cause memory exhaustion DoS)
#define COOKIE_LEN 32
static const char *TAG = "session";
struct session {
char cookie[COOKIE_LEN + 1];
void *data;
TickType_t last_activity_time;
LIST_ENTRY(session) link;
sess_data_free_fn_t free_fn;
};
static LIST_HEAD(sessions_, session) s_store;
static SemaphoreHandle_t sess_store_lock = NULL;
static bool sess_store_inited = false;
void session_store_init(void)
{
if (sess_store_inited) {
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
struct session *it, *tit;
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
ESP_LOGW(TAG, "Session cookie expired: \"%s\"", it->cookie);
if (it->free_fn) it->free_fn(it->data);
// no relink, we dont care if the list breaks after this - we're removing all of it
free(it);
}
}
LIST_INIT(&s_store);
xSemaphoreGive(sess_store_lock);
} else {
LIST_INIT(&s_store);
sess_store_lock = xSemaphoreCreateMutex();
sess_store_inited = true;
}
}
/**
* Fill buffer with base64 symbols. Does not add a trailing null byte
*
* @param buf
* @param len
*/
static void esp_fill_random_alnum(char *buf, size_t len)
{
#define alphabet_len 64
static const char alphabet[alphabet_len] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
unsigned int seed = xTaskGetTickCount();
assert(buf != NULL);
for (int i = 0; i < len; i++) {
int index = rand_r(&seed) % alphabet_len;
*buf++ = (uint8_t) alphabet[index];
}
}
const char *session_new(void *data, sess_data_free_fn_t free_fn)
{
assert(data != NULL);
struct session *item = malloc(sizeof(struct session));
if (item == NULL) return NULL;
item->data = data;
item->free_fn = free_fn;
esp_fill_random_alnum(item->cookie, COOKIE_LEN);
item->cookie[COOKIE_LEN] = 0; // add the terminator
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
item->last_activity_time = xTaskGetTickCount();
LIST_INSERT_HEAD(&s_store, item, link);
}
xSemaphoreGive(sess_store_lock);
ESP_LOGD(TAG, "New HTTP session: %s", item->cookie);
return item->cookie;
}
void *session_find_and(const char *cookie, enum session_find_action action)
{
// no point in searching if the length is wrong
if (strlen(cookie) != COOKIE_LEN) {
ESP_LOGW(TAG, "Wrong session cookie length: \"%s\"", cookie);
return NULL;
}
struct session *it = NULL;
bool found = false;
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
LIST_FOREACH(it, &s_store, link) {
if (0==strcmp(it->cookie, cookie)) {
ESP_LOGD(TAG, "Session cookie matched: \"%s\"", cookie);
it->last_activity_time = xTaskGetTickCount();
found = true;
break;
}
}
if (found && action == SESS_DROP) {
if (it->free_fn) it->free_fn(it->data);
LIST_REMOVE(it, link);
free(it);
ESP_LOGD(TAG, "Dropped session: \"%s\"", cookie);
}
}
xSemaphoreGive(sess_store_lock);
if (found) {
if (action == SESS_DROP) {
// it was dropped inside the guarded block
// the return value is not used with DROP
return NULL;
}
else if(action == SESS_GET_DATA) {
return it->data;
}
}
ESP_LOGW(TAG, "Session cookie not found: \"%s\"", cookie);
return NULL;
}
void *session_find(const char *cookie)
{
return session_find_and(cookie, SESS_GET_DATA);
}
void session_drop(const char *cookie)
{
session_find_and(cookie, SESS_DROP);
}
void session_drop_expired(void)
{
struct session *it;
struct session *tit;
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
TickType_t now = xTaskGetTickCount();
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
TickType_t elapsed = now - it->last_activity_time;
if (elapsed > pdMS_TO_TICKS(SESSION_EXPIRY_TIME_S*1000)) {
ESP_LOGD(TAG, "Session cookie expired: \"%s\"", it->cookie);
if (it->free_fn) it->free_fn(it->data);
LIST_REMOVE(it, link);
free(it);
}
}
}
xSemaphoreGive(sess_store_lock);
}
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action)
{
// this could be called periodically, but it's sufficient to run it at each request
// it won't slow anything down unless there are hundreds of sessions
session_drop_expired();
static char buf[256];
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Cookie", buf, 256);
if (rv == ESP_OK || rv == ESP_ERR_HTTPD_RESULT_TRUNC) {
ESP_LOGD(TAG, "Cookie header: %s", buf);
// probably OK, see if we have a cookie
char *start = strstr(buf, SESSION_COOKIE_NAME"=");
if (start != 0) {
start += strlen(SESSION_COOKIE_NAME"=");
char *end = strchr(start, ';');
if (end != NULL) *end = 0;
ESP_LOGD(TAG, "Cookie is: %s", start);
return session_find_and(start, action);
}
} else {
ESP_LOGD(TAG, "No cookie.");
}
return NULL;
}
void httpd_resp_delete_session_cookie(httpd_req_t *r)
{
httpd_resp_set_hdr(r, "Set-Cookie", SESSION_COOKIE_NAME"=");
}
// Static because the value is passed and stored by reference, so it wouldn't live long enough if it was on stack,
// and there also isn't any hook for freeing it if we used malloc(). This is an SDK bug.
static char cookie_hdr_buf[COOKIE_LEN + 10];
// !!! this must not be called concurrently from a different thread.
// That is no problem so long as the server stays single-threaded
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie)
{
snprintf(cookie_hdr_buf, COOKIE_LEN + 10, "SESSID=%s", cookie);
httpd_resp_set_hdr(r, "Set-Cookie", cookie_hdr_buf);
}
@@ -0,0 +1,41 @@
/**
* TODO file description
*
* Created on 2019/07/13.
*/
#ifndef SESSION_UTILS_C_H
#define SESSION_UTILS_C_H
#include <esp_err.h>
#include <esp_http_server.h>
#include "httpd_utils/session_kvmap.h"
#include "httpd_utils/session_store.h"
/**
* Start or resume a session.
*/
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **ppkvstore, httpd_req_t *r)
{
sess_kv_map_t *kvstore;
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA);
if (NULL == kvstore) {
kvstore = sess_kv_map_alloc();
if (!kvstore) return ESP_ERR_NO_MEM;
const char *cookie = session_new(kvstore, sess_kv_map_free);
if (cookie == NULL) {
// session alloc failed
sess_kv_map_free(kvstore);
*ppkvstore = NULL;
return ESP_ERR_NO_MEM;
}
httpd_resp_set_session_cookie(r, cookie);
}
*ppkvstore = kvstore;
return ESP_OK;
}
#endif //SESSION_UTILS_C_H