//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG #include #include #include #include #include #include #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 = calloc(sizeof(struct session), 1); 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); }