add webserver components, some webserver fices and improvements in templating, add real time history chart
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
set(COMPONENT_ADD_INCLUDEDIRS
|
||||
"include")
|
||||
|
||||
set(COMPONENT_SRCDIRS
|
||||
"src")
|
||||
|
||||
set(COMPONENT_REQUIRES tcpip_adapter esp_http_server common_utils)
|
||||
|
||||
register_component()
|
||||
@@ -0,0 +1,4 @@
|
||||
Functions enriching the HTTP server bundled with ESP-IDF.
|
||||
This package includes HTTP-related utilities, captive
|
||||
portal implementation, and a cookie-based session system with a
|
||||
key-value store and expirations.
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
COMPONENT_SRCDIRS := src
|
||||
COMPONENT_ADD_INCLUDEDIRS := include
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef HTTPD_UTILS_CAPTIVE_H
|
||||
#define HTTPD_UTILS_CAPTIVE_H
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
/**
|
||||
* Redirect if needed when a captive portal capture is detected
|
||||
*
|
||||
* @param r
|
||||
* @return ESP_OK on redirect, ESP_ERR_NOT_FOUND if not needed, other error on failure
|
||||
*/
|
||||
esp_err_t httpd_captive_redirect(httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Get URL to redirect to. WEAK.
|
||||
*
|
||||
* @param r
|
||||
* @param buf
|
||||
* @param maxlen
|
||||
* @return http(s)://foo.bar/
|
||||
*/
|
||||
esp_err_t httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen);
|
||||
|
||||
/**
|
||||
* Get captive portal domain. WEAK.
|
||||
*
|
||||
* @return foo.bar
|
||||
*/
|
||||
const char * httpd_captive_redirect_get_domain();
|
||||
|
||||
#endif //HTTPD_UTILS_CAPTIVE_H
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef HTTPD_FDIPV4_H
|
||||
#define HTTPD_FDIPV4_H
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
#endif //HTTPD_FDIPV4_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef HTTPD_UTILS_REDIRECT_H
|
||||
#define HTTPD_UTILS_REDIRECT_H
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
/**
|
||||
* Redirect to other URI - sends a HTTP response from the http server
|
||||
*
|
||||
* @param r - request
|
||||
* @param uri - target uri
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri);
|
||||
|
||||
#endif // HTTPD_UTILS_REDIRECT_H
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Session store system main include file
|
||||
*
|
||||
* Created on 2019/07/13.
|
||||
*/
|
||||
|
||||
#ifndef HTTPD_UTILS_SESSION_H
|
||||
#define HTTPD_UTILS_SESSION_H
|
||||
|
||||
#include "session_kvmap.h"
|
||||
#include "session_store.h"
|
||||
|
||||
// Customary keys
|
||||
|
||||
/** Session key for OK flash message */
|
||||
#define SESS_FLASH_OK "flash_ok"
|
||||
/** Session key for error flash message */
|
||||
#define SESS_FLASH_ERR "flash_err"
|
||||
/** Session key for a "logged in" flag. Value is 1 if logged in. */
|
||||
#define SESS_AUTHED "authed"
|
||||
|
||||
// ..
|
||||
|
||||
/**
|
||||
* Redirect to /login form if unauthed,
|
||||
* but also retrieve the session key-value store for further use
|
||||
*/
|
||||
#define HTTP_GET_AUTHED_SESSION(kvstore, r) do { \
|
||||
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA); \
|
||||
if (NULL == kvstore || NULL == sess_kv_map_get(kvstore, SESS_AUTHED)) { \
|
||||
return httpd_redirect_to((r), "/login"); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* Start or resume a session without checking for authed status.
|
||||
* When started, the session cookie is added to the response immediately.
|
||||
*
|
||||
* @param[out] kvstore - a place to store the allocated or retrieved session kvmap
|
||||
* @param[in] r - request
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **kvstore, httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Redirect to the login form if unauthed.
|
||||
* This is the same as `HTTP_GET_AUTHED_SESSION`, except the kvstore variable is
|
||||
* not needed in the uri handler calling this, so it is declared internally.
|
||||
*/
|
||||
#define HTTP_REDIRECT_IF_UNAUTHED(r) do { \
|
||||
sess_kv_map_t *_kvstore; \
|
||||
HTTP_GET_AUTHED_SESSION(_kvstore, r); \
|
||||
} while(0)
|
||||
|
||||
#endif // HTTPD_UTILS_SESSION_H
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Simple key-value map for session data storage.
|
||||
* Takes care of dynamic allocation and cleanup.
|
||||
*
|
||||
* Created on 2019/01/28.
|
||||
*/
|
||||
|
||||
#ifndef SESSION_KVMAP_H
|
||||
#define SESSION_KVMAP_H
|
||||
|
||||
/**
|
||||
* Prototype for a free() func to clean up session-held objects
|
||||
*/
|
||||
typedef void (*sess_kv_free_func_t)(void *obj);
|
||||
|
||||
typedef struct sess_kv_map sess_kv_map_t;
|
||||
|
||||
#define SESS_KVMAP_KEY_LEN 16
|
||||
|
||||
/**
|
||||
* Allocate a new session key-value store
|
||||
*
|
||||
* @return the store, NULL on error
|
||||
*/
|
||||
sess_kv_map_t *sess_kv_map_alloc(void);
|
||||
|
||||
/**
|
||||
* Free the session kv store.
|
||||
*
|
||||
* @param head - store head
|
||||
*/
|
||||
void sess_kv_map_free(void *head);
|
||||
|
||||
/**
|
||||
* Get a value from the session kv store.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to get a value for
|
||||
* @return the value, or NULL if not found
|
||||
*/
|
||||
void *sess_kv_map_get(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Get and remove a value from the session store.
|
||||
*
|
||||
* The free function is not called in this case and the recipient is
|
||||
* responsible for cleaning it up correctly.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to get a value for
|
||||
* @return the value, or NULL if not found
|
||||
*/
|
||||
void * sess_kv_map_take(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Remove an entry from the session by its key name.
|
||||
* The slot is not free'd yet, but is made available for reuse.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to remove
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t sess_kv_map_remove(sess_kv_map_t *head, const char *key);
|
||||
|
||||
/**
|
||||
* Set a key value. If there is an old value stored, it will be freed by its free function and replaced by the new one.
|
||||
* Otherwise a new slot is allocated for it, or a previously released one is reused.
|
||||
*
|
||||
* @param head - store head
|
||||
* @param key - key to assign to
|
||||
* @param value - new value
|
||||
* @param free_fn - value free func
|
||||
* @return success
|
||||
*/
|
||||
esp_err_t sess_kv_map_set(sess_kv_map_t *head, const char *key, void *value, sess_kv_free_func_t free_fn);
|
||||
|
||||
#endif //SESSION_KVMAP_H
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Cookie-based session store
|
||||
*/
|
||||
|
||||
#ifndef SESSION_STORE_H
|
||||
#define SESSION_STORE_H
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#define SESSION_EXPIRY_TIME_S 60*30
|
||||
#define SESSION_COOKIE_NAME "SESSID"
|
||||
|
||||
/** function that frees a session data object */
|
||||
typedef void (*sess_data_free_fn_t)(void *);
|
||||
|
||||
enum session_find_action {
|
||||
SESS_DROP, SESS_GET_DATA
|
||||
};
|
||||
|
||||
/**
|
||||
* Find session and either get data, or drop it.
|
||||
*
|
||||
* @param cookie
|
||||
* @param action
|
||||
* @return
|
||||
*/
|
||||
void *session_find_and(const char *cookie, enum session_find_action action);
|
||||
|
||||
/**
|
||||
* Initialize the session store.
|
||||
* Safely empty it if initialized
|
||||
*/
|
||||
void session_store_init(void);
|
||||
|
||||
// placeholder for when no data is stored
|
||||
#define SESSION_DUMMY ((void *) 1)
|
||||
|
||||
/**
|
||||
* Create a new session. Data must not be NULL, because it wouldn't be possible
|
||||
* to distinguish between NULL value and session not found in return values.
|
||||
* It can be e.g. 1 if no data storage is needed.
|
||||
*
|
||||
* @param data - data object to attach to the session
|
||||
* @param free_fn - function that disposes of the data when the session expires
|
||||
* @return NULL on error, or the new session ID. This is a live pointer into the session structure,
|
||||
* must be copied if stored, as it can become invalid at any time
|
||||
*/
|
||||
const char *session_new(void *data, sess_data_free_fn_t free_fn);
|
||||
|
||||
/**
|
||||
* Find a session by it's ID (from a cookie)
|
||||
*
|
||||
* @param cookie - session ID string
|
||||
* @return session data (void*), or NULL
|
||||
*/
|
||||
void *session_find(const char *cookie);
|
||||
|
||||
/**
|
||||
* Loop through all sessions and drop these that expired.
|
||||
*/
|
||||
void session_drop_expired(void);
|
||||
|
||||
/**
|
||||
* Drop a session by its ID. Does nothing if not found.
|
||||
*
|
||||
* @param cookie - session ID string
|
||||
*/
|
||||
void session_drop(const char *cookie);
|
||||
|
||||
/**
|
||||
* Parse the Cookie header from a request, and do something with the corresponding session.
|
||||
*
|
||||
* To also delete the cookie, use req_delete_session_cookie(r)
|
||||
*
|
||||
* @param r - request
|
||||
* @param action - what to do with the session
|
||||
* @return session data, NULL if removed or not found
|
||||
*/
|
||||
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action);
|
||||
|
||||
/**
|
||||
* Add a header that deletes the session cookie
|
||||
*
|
||||
* @param r - request
|
||||
*/
|
||||
void httpd_resp_delete_session_cookie(httpd_req_t *r);
|
||||
|
||||
/**
|
||||
* Add a header that sets the session cookie.
|
||||
*
|
||||
* This must be called after creating a session (e.g. user logged in) to make it persistent.
|
||||
*
|
||||
* @attention NOT RE-ENTRANT, CAN'T BE USED AGAIN UNTIL THE REQUEST IT WAS CALLED FOR IS DISPATCHED.
|
||||
*
|
||||
* @param r - request
|
||||
* @param cookie - cookie ID
|
||||
*/
|
||||
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie);
|
||||
|
||||
#endif //SESSION_STORE_H
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user