diff --git a/components/common_utils/CMakeLists.txt b/components/common_utils/CMakeLists.txt new file mode 100644 index 0000000..065ee43 --- /dev/null +++ b/components/common_utils/CMakeLists.txt @@ -0,0 +1,8 @@ +set(COMPONENT_ADD_INCLUDEDIRS include) + +set(COMPONENT_SRCDIRS + "src") + +#set(COMPONENT_REQUIRES) + +register_component() diff --git a/components/common_utils/README.txt b/components/common_utils/README.txt new file mode 100644 index 0000000..48f10c0 --- /dev/null +++ b/components/common_utils/README.txt @@ -0,0 +1,2 @@ +General purpose, mostly platofrm-idependent utilities +that may be used by other components. diff --git a/components/common_utils/component.mk b/components/common_utils/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/common_utils/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/common_utils/include/common_utils/base16.h b/components/common_utils/include/common_utils/base16.h new file mode 100644 index 0000000..c3fc01d --- /dev/null +++ b/components/common_utils/include/common_utils/base16.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 Michael Brown . + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef BASE16_H_ +#define BASE16_H_ + +#include +#include + +/** + * Calculate length of base16-encoded data + * @param raw_len Raw data length + * @return Encoded string length (excluding NUL) + */ +static inline size_t base16_encoded_len(size_t raw_len) { + return (2 * raw_len); +} + +/** + * Calculate maximum length of base16-decoded string + * @param encoded Encoded string + * @return Maximum length of raw data + */ +static inline size_t base16_decoded_max_len(const char *encoded) { + return ((strlen(encoded) + 1) / 2); +} + +/** + * Base16-encode data + * + * The buffer must be the correct length for the encoded string. Use + * something like + * + * char buf[ base16_encoded_len ( len ) + 1 ]; + * + * (the +1 is for the terminating NUL) to provide a buffer of the + * correct size. + * + * @param raw Raw data + * @param len Length of raw data + * @param encoded Buffer for encoded string + */ +extern void base16_encode(uint8_t *raw, size_t len, char *encoded); + +/** + * Base16-decode data + * + * The buffer must be large enough to contain the decoded data. Use + * something like + * + * char buf[ base16_decoded_max_len ( encoded ) ]; + * + * to provide a buffer of the correct size. + * @param encoded Encoded string + * @param raw Raw data + * @return Length of raw data, or negative error + */ +extern int base16_decode(const char *encoded, uint8_t *raw); + +#endif /* BASE16_H_ */ diff --git a/components/common_utils/include/common_utils/datetime.h b/components/common_utils/include/common_utils/datetime.h new file mode 100644 index 0000000..814b779 --- /dev/null +++ b/components/common_utils/include/common_utils/datetime.h @@ -0,0 +1,131 @@ +/** + * TODO file description + * + * Created on 2019/09/13. + */ + +#ifndef CSPEMU_DATETIME_H +#define CSPEMU_DATETIME_H + +#include +#include + +enum weekday { + MONDAY = 1, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, + SUNDAY +}; +_Static_assert(MONDAY==1, "enum weekday numbering Mon"); +_Static_assert(SUNDAY==7, "enum weekday numbering Sun"); + +enum month { + JANUARY = 1, + FEBRUARY, + MARCH, + APRIL, + MAY, + JUNE, + JULY, + AUGUST, + SEPTEMBER, + OCTOBER, + NOVEMBER, + DECEMBER +}; +_Static_assert(JANUARY==1, "enum month numbering Jan"); +_Static_assert(DECEMBER==12, "enum month numbering Dec"); + +/** Abbreviated weekday names */ +extern const char *DT_WKDAY_NAMES[]; +/** Full-length weekday names */ +extern const char *DT_WKDAY_NAMES_FULL[]; +/** Abbreviated month names */ +extern const char *DT_MONTH_NAMES[]; +/** Full-length month names */ +extern const char *DT_MONTH_NAMES_FULL[]; + +typedef struct datetime { + uint16_t year; + enum month month; + uint8_t day; + uint8_t hour; + uint8_t min; + uint8_t sec; + enum weekday wkday; // 1=monday +} datetime_t; + +// Templates for printf +#define DT_FORMAT_DATE "%d/%d/%d" +#define DT_SUBS_DATE(dt) (dt).year, (dt).month, (dt).day + +#define DT_FORMAT_TIME "%d:%02d:%02d" +#define DT_SUBS_TIME(dt) (dt).hour, (dt).min, (dt).sec + +#define DT_FORMAT_DATE_WK DT_FORMAT_WK " " DT_FORMAT_DATE +#define DT_SUBS_DATE_WK(dt) DT_SUBS_WK(dt), DT_SUBS_DATE(dt) + +#define DT_FORMAT_WK "%s" +#define DT_SUBS_WK(dt) DT_WKDAY_NAMES[(dt).wkday] + +#define DT_FORMAT_DATE_TIME DT_FORMAT_DATE " " DT_FORMAT_TIME +#define DT_SUBS_DATE_TIME(dt) DT_SUBS_DATE(dt), DT_SUBS_TIME(dt) + +#define DT_FORMAT DT_FORMAT_DATE_WK " " DT_FORMAT_TIME +#define DT_SUBS(dt) DT_SUBS_DATE_WK(dt), DT_SUBS_TIME(dt) + +// base century for two-digit year conversions +#define DT_CENTURY 2000 +// start year for weekday computation +#define DT_START_YEAR 2019 +// January 1st weekday of DT_START_YEAR +#define DT_START_WKDAY TUESDAY +// max date supported by 2-digit year RTC counters (it can't check Y%400==0 with only two digits) +#define DT_END_YEAR 2399 + +typedef union __attribute__((packed)) { + struct __attribute__((packed)) { + uint8_t ones : 4; + uint8_t tens : 4; + }; + uint8_t byte; +} bcd_t; +_Static_assert(sizeof(bcd_t) == 1, "Bad bcd_t len"); + +/** Check if a year is leap */ +static inline bool is_leap_year(int year) +{ + return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0); +} + +/** + * Check if a datetime could be valid (ignores leap years) + * + * @param[in] dt + * @return basic validations passed + */ +bool datetime_is_valid(const datetime_t *dt); + +/** + * Set weekday based on a date in a given datetime + * + * @param[in,out] dt + * @return success + */ +bool datetime_set_weekday(datetime_t *dt); + +/** + * Get weekday for given a date + * + * @param year - year number + * @param month - 1-based month number + * @param day - 1-based day number + * @return weekday + */ +enum weekday date_weekday(uint16_t year, enum month month, uint8_t day); + + +#endif //CSPEMU_DATETIME_H diff --git a/components/common_utils/include/common_utils/hexdump.h b/components/common_utils/include/common_utils/hexdump.h new file mode 100644 index 0000000..1420de1 --- /dev/null +++ b/components/common_utils/include/common_utils/hexdump.h @@ -0,0 +1,19 @@ +/** + * @file + * @brief A simple way of dumping memory to a hex output + * + * \addtogroup Hexdump + * + * @{ + */ + + +#include + +#define HEX_DUMP_LINE_BUFF_SIZ 16 + +extern void hex_dump(FILE * fp,void *src, int len); +extern void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len); +/** + * }@ + */ diff --git a/components/common_utils/include/common_utils/utils.h b/components/common_utils/include/common_utils/utils.h new file mode 100644 index 0000000..27f72b6 --- /dev/null +++ b/components/common_utils/include/common_utils/utils.h @@ -0,0 +1,101 @@ +/** + * General purpose, platform agnostic, reusable utils + */ + +#ifndef COMMON_UTILS_UTILS_H +#define COMMON_UTILS_UTILS_H + +#include +#include + +#include "base16.h" +#include "datetime.h" +#include "hexdump.h" + +/** Convert a value to BCD struct */ +static inline bcd_t num2bcd(uint8_t value) +{ + return (bcd_t) {.ones=value % 10, .tens=value / 10}; +} + +/** Convert unpacked BCD to value */ +static inline uint8_t bcd2num(uint8_t tens, uint8_t ones) +{ + return tens * 10 + ones; +} + +/** + * Append to a buffer. + * + * In case the buffer capacity is reached, it is reverted to the previous length (by replacing the NUL byte) + * and NULL is returned. + * + * @param buf - buffer position to append at; if NULL is given, the function immediately returns NULL. + * @param appended - string to append + * @param pcap - pointer to a capacity variable + * @return the new end of the string (null byte); use as 'buf' for following appends + */ +char *append(char *buf, const char *appended, size_t *pcap); + +/** + * Append, re-allocating as needed. + * + * @param head - pointer to heap buffer head, may be updated on realloc. + * @param size - string size pointer, may be updated on realloc. + * @param appended - string to append + * @return false on alloc error + */ +bool append_realloc(char **head, size_t *cap, const char *appended); + +/** + * Test if a file descriptor is valid (e.g. when cleaning up after a failed select) + * + * @param fd - file descriptor number + * @return is valid + */ +bool fd_is_valid(int fd); + +/** + * parse user-provided string as boolean + * + * @param str + * @return is true + */ +bool parse_boolean_arg(const char *str); + +/** + * complementary function to parse_boolean_arg() that matches strings + * meaning 'false'. Can be used together with the positive version + * in case there can be other values as well. + * + * @param str + * @return is false + */ +bool parse_boolean_arg_false(const char *str); + +/** Check equality of two strings; returns bool */ +#define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0) + +/** Check prefix equality of two strings; returns bool */ +#define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0) + +/** Check if a string starts with a substring; returns bool */ +#define strstarts(a, b) strneq((a), (b), (int)strlen((b))) + +#ifndef MIN +/** Get min of two numbers */ +#define MIN(a, b) ((a) > (b) ? (b) : (a)) +#endif + +#ifndef MAX +/** Get max of two values */ +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef STR +#define STR_HELPER(x) #x +/** Stringify a token */ +#define STR(x) STR_HELPER(x) +#endif + +#endif //COMMON_UTILS_UTILS_H diff --git a/components/common_utils/src/base16.c b/components/common_utils/src/base16.c new file mode 100644 index 0000000..6f13d0a --- /dev/null +++ b/components/common_utils/src/base16.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 Michael Brown . + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include +#include +#include +#include + +static const char *TAG = "base16"; + +void base16_encode(uint8_t *raw, size_t len, char *encoded) { + uint8_t *raw_bytes = raw; + char *encoded_bytes = encoded; + size_t remaining = len; + + for (; remaining--; encoded_bytes += 2) + snprintf(encoded_bytes, 3, "%02X", *(raw_bytes++)); + +} + +int base16_decode(const char *encoded, uint8_t *raw) { + const char *encoded_bytes = encoded; + uint8_t *raw_bytes = raw; + char buf[3]; + char *endp; + size_t len; + + while (encoded_bytes[0]) { + if (!encoded_bytes[1]) { + ESP_LOGE(TAG, "Base16-encoded string \"%s\" has invalid length\n", + encoded); + return -22; + } + memcpy(buf, encoded_bytes, 2); + buf[2] = '\0'; + *(raw_bytes++) = strtoul(buf, &endp, 16); + if (*endp != '\0') { + ESP_LOGE(TAG,"Base16-encoded string \"%s\" has invalid byte \"%s\"\n", + encoded, buf); + return -22; + } + encoded_bytes += 2; + } + len = (raw_bytes - raw); + return (len); +} diff --git a/components/common_utils/src/common_utils.c b/components/common_utils/src/common_utils.c new file mode 100644 index 0000000..d91aaf4 --- /dev/null +++ b/components/common_utils/src/common_utils.c @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include +#include + +#include "common_utils/utils.h" + +char *append(char *buf, const char *appended, size_t *pcap) +{ + char c; + char *buf0 = buf; + size_t cap = *pcap; + + if (buf0 == NULL) return NULL; + if (appended == NULL || appended[0] == 0) return buf0; + + if (*pcap < strlen(appended)+1) { + return NULL; + } + + while (cap > 1 && 0 != (c = *appended++)) { + *buf++ = c; + cap--; + } + assert(cap > 0); + + *pcap = cap; + *buf = 0; + return buf; +} + +bool append_realloc(char **head, size_t *cap, const char *appended) { + if (!head) return NULL; + if (!*head) return NULL; + if (!cap) return NULL; + if (!appended) return NULL; + + size_t cursize = strlen(*head); + size_t needed = strlen(appended) + 1; + size_t remains = *cap - cursize; + + if (remains < needed) { + size_t need_extra = needed - remains; + size_t newsize = *cap + need_extra; + char *new = realloc(*head, newsize); + if (!new) return false; + *head = new; + *cap = newsize; + } + + strcpy(*head + cursize, appended); + return true; +} + +bool fd_is_valid(int fd) +{ + return fcntl(fd, F_GETFD) != -1 || errno != EBADF; +} + +bool parse_boolean_arg(const char *str) +{ + if (0 == strcasecmp(str, "on")) return true; + if (0 == strcmp(str, "1")) return true; + if (0 == strcasecmp(str, "yes")) return true; + if (0 == strcasecmp(str, "enable")) return true; + if (0 == strcasecmp(str, "en")) return true; + if (0 == strcasecmp(str, "y")) return true; + if (0 == strcasecmp(str, "a")) return true; + + return false; +} + +bool parse_boolean_arg_false(const char *str) +{ + if (0 == strcasecmp(str, "off")) return true; + if (0 == strcmp(str, "0")) return true; + if (0 == strcasecmp(str, "no")) return true; + if (0 == strcasecmp(str, "disable")) return true; + if (0 == strcasecmp(str, "dis")) return true; + if (0 == strcasecmp(str, "n")) return true; + + return false; +} diff --git a/components/common_utils/src/datetime.c b/components/common_utils/src/datetime.c new file mode 100644 index 0000000..b2fede4 --- /dev/null +++ b/components/common_utils/src/datetime.c @@ -0,0 +1,110 @@ + +#include +#include +#include +#include "common_utils/datetime.h" + +const char *DT_WKDAY_NAMES[] = { + [MONDAY] = "Mon", + [TUESDAY] = "Tue", + [WEDNESDAY] = "Wed", + [THURSDAY] = "Thu", + [FRIDAY] = "Fri", + [SATURDAY] = "Sat", + [SUNDAY] = "Sun" +}; + +const char *DT_WKDAY_NAMES_FULL[] = { + [MONDAY] = "Monday", + [TUESDAY] = "Tuesday", + [WEDNESDAY] = "Wednesday", + [THURSDAY] = "Thursday", + [FRIDAY] = "Friday", + [SATURDAY] = "Saturday", + [SUNDAY] = "Sunday" +}; + +const char *DT_MONTH_NAMES[] = { + [JANUARY] = "Jan", + [FEBRUARY] = "Feb", + [MARCH] = "Mar", + [APRIL] = "Apr", + [MAY] = "May", + [JUNE] = "Jun", + [JULY] = "Jul", + [AUGUST] = "Aug", + [SEPTEMBER] = "Sep", + [OCTOBER] = "Oct", + [NOVEMBER] = "Nov", + [DECEMBER] = "Dec" +}; + +const char *DT_MONTH_NAMES_FULL[] = { + [JANUARY] = "January", + [FEBRUARY] = "February", + [MARCH] = "March", + [APRIL] = "April", + [MAY] = "May", + [JUNE] = "June", + [JULY] = "July", + [AUGUST] = "August", + [SEPTEMBER] = "September", + [OCTOBER] = "October", + [NOVEMBER] = "November", + [DECEMBER] = "December" +}; + +static const uint16_t MONTH_LENGTHS[] = { /* 1-based, normal year */ + [JANUARY]=31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +}; +_Static_assert(sizeof(MONTH_LENGTHS) / sizeof(uint16_t) == 13, "Months array length"); + +static const uint16_t MONTH_YEARDAYS[] = { /* 1-based */ + [JANUARY]=0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 // // days until 1st of month +}; +_Static_assert(sizeof(MONTH_YEARDAYS) / sizeof(uint16_t) == 13, "Months array length"); + +_Static_assert(MONDAY < SUNDAY, "Weekday ordering"); + +bool datetime_is_valid(const datetime_t *dt) +{ + if (dt == NULL) return false; + + // check month first to avoid out-of-bounds read from the MONTH_LENGTHS table + if (!(dt->month >= JANUARY && dt->month <= DECEMBER)) return false; + + int monthlen = MONTH_LENGTHS[dt->month]; + if (dt->month == FEBRUARY && is_leap_year(dt->year)) { + monthlen = 29; + } + + return dt->sec < 60 && + dt->min < 60 && + dt->hour < 24 && + dt->wkday >= MONDAY && + dt->wkday <= SUNDAY && + dt->year >= DT_START_YEAR && + dt->year <= DT_END_YEAR && + dt->day >= 1 && + dt->day <= monthlen; +} + +bool datetime_set_weekday(datetime_t *dt) +{ + dt->wkday = MONDAY; // prevent the validator func erroring out on invalid weekday + if (!datetime_is_valid(dt)) return false; + dt->wkday = date_weekday(dt->year, dt->month, dt->day); + return true; +} + +enum weekday date_weekday(uint16_t year, enum month month, uint8_t day) +{ + uint16_t days = (DT_START_WKDAY - MONDAY) + (year - DT_START_YEAR) * 365 + MONTH_YEARDAYS[month] + (day - 1); + + for (uint16_t i = DT_START_YEAR; i <= year; i++) { + if (is_leap_year(i) && (i < year || month > FEBRUARY)) days++; + } + + return MONDAY + days % 7; +} + diff --git a/components/common_utils/src/hexdump.c b/components/common_utils/src/hexdump.c new file mode 100644 index 0000000..cbf90c2 --- /dev/null +++ b/components/common_utils/src/hexdump.c @@ -0,0 +1,72 @@ +/* + * util.c + * + * Created on: Aug 12, 2009 + * Author: johan + */ + +// adapted from libgomspace + +#include +#include +#include "common_utils/hexdump.h" + + +//! Dump memory to debugging output +/** + * Dumps a chunk of memory to the screen + */ +void hex_dump(FILE * fp, void *src, int len) { + int i, j=0, k; + char text[17]; + + text[16] = '\0'; + //printf("Hex dump:\r\n"); + fprintf(fp, "%p : ", src); + for(i=0; i 126)) { + text[k] = '.'; + } + } + fprintf(fp, " |%s|\n\r", text); + if(i= 32 && line[i] <= 126) + fprintf(fp, "%c", (unsigned char)line[i]); + else + fputc('.', fp); + } + fputs("|\r\n", fp); +} diff --git a/components/fileserver/CMakeLists.txt b/components/fileserver/CMakeLists.txt new file mode 100644 index 0000000..b02af21 --- /dev/null +++ b/components/fileserver/CMakeLists.txt @@ -0,0 +1,9 @@ +set(COMPONENT_ADD_INCLUDEDIRS + "include") + +set(COMPONENT_SRCDIRS + "src") + +set(COMPONENT_REQUIRES tcpip_adapter esp_http_server httpd_utils common_utils) + +register_component() diff --git a/components/fileserver/README.txt b/components/fileserver/README.txt new file mode 100644 index 0000000..588ad42 --- /dev/null +++ b/components/fileserver/README.txt @@ -0,0 +1,2 @@ +File and template serving support for the http_server bundled with ESP-IDF. + diff --git a/components/fileserver/component.mk b/components/fileserver/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/fileserver/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/fileserver/include/fileserver/embedded_files.h b/components/fileserver/include/fileserver/embedded_files.h new file mode 100644 index 0000000..a86f8ae --- /dev/null +++ b/components/fileserver/include/fileserver/embedded_files.h @@ -0,0 +1,51 @@ +// +// Created on 2018/10/17 by Ondrej Hruska +// + +#ifndef FBNODE_EMBEDDED_FILES_H +#define FBNODE_EMBEDDED_FILES_H + +#include +#include +#include + +struct embedded_file_info { + const uint8_t * start; + const uint8_t * end; + const char * name; + const char * mime; +}; + +enum file_access_level { + /** Public = file accessed by a wildcard route */ + FILE_ACCESS_PUBLIC = 0, + /** Protected = file included in a template or explicitly specified in a route */ + FILE_ACCESS_PROTECTED = 1, + /** Files protected against read-out */ + FILE_ACCESS_PRIVATE = 2, +}; + +extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[]; +extern const size_t EMBEDDED_FILE_LOOKUP_LEN; + +/** + * Find an embedded file by its name. + * + * This function is weak. It crawls the EMBEDDED_FILE_LOOKUP table and checks for exact match, also + * testing with www_get_static_file_access_check if the access is allowed. + * + * @param name - file name + * @param access - access level (public - wildcard fallthrough, protected - files for the server, loaded explicitly) + * @param[out] file - the file struct is stored here if found, unchanged if not found. + * @return status code + */ +esp_err_t www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file); + +/** + * Check file access permission (if using the default www_get_static_file implementation). + * + * This function is weak. The default implementation returns always true. + */ +bool www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access); + +#endif //FBNODE_EMBEDDED_FILES_H diff --git a/components/fileserver/include/fileserver/token_subs.h b/components/fileserver/include/fileserver/token_subs.h new file mode 100644 index 0000000..f153c42 --- /dev/null +++ b/components/fileserver/include/fileserver/token_subs.h @@ -0,0 +1,227 @@ +// +// This module implements token substitution in files served by the server. +// +// Tokens are in the form {token}, or {escape:token}, where escape can be: +// - h ... html escape (plain text in a html file, attribute value) +// - j ... js escape (for use in JS strings) +// +// When no escape is specified, the token substitution is written verbatim into the response. +// +// var foo = "{j:foo}"; +// +// {generated-html-goes-here} +// +// Token can be made optional by adding '?' at the end (this can't be used for includes). +// Such token then simply becomes empty string when not substituted, as opposed to being included in the page verbatim. +// +// +// +// token names can contain alnum, dash, period and underscore, and are case sensitive. +// +// +// It is further possible to include a static file with optional key-value replacements. These serve as defaults. +// +// {@_subfile.html} +// {@_subfile.html|key=value lalala} +// {@_subfile.html|key=value lalala|other=value} +// +// File inclusion can be nested, and the files can use replacement tokens as specified by the include statement +// +// Created on 2019/01/24 by Ondrej Hruska +// + +#ifndef FBNODE_TOKEN_SUBS_H +#define FBNODE_TOKEN_SUBS_H + +#include "embedded_files.h" +#include +#include +#include + +/** Max length of a token buffer (must suffice for all included filenames) */ +#define MAX_TOKEN_LEN 32 + +/** Max length of a key-value substitution when using tpl_kv_replacer; + * This is also used internally for in-line replacements in file imports. */ +#define TPL_KV_KEY_LEN 24 +/** Max length of a substituion in tpl_kv_replacer */ +#define TPL_KV_SUBST_LEN 64 + +/** + * Escape type - argument for httpd_resp_send_chunk_escaped() + */ +typedef enum { + TPL_ESCAPE_NONE = 0, + TPL_ESCAPE_HTML, + TPL_ESCAPE_JS, +} tpl_escape_t; + +enum { + HTOPT_NONE = 0, + HTOPT_NO_HEADERS = 1 << 0, + HTOPT_NO_CLOSE = 1 << 1, + HTOPT_INCLUDE = HTOPT_NO_HEADERS|HTOPT_NO_CLOSE, +}; + +/** + * Send string using a given escaping scheme + * + * @param r + * @param buf - buf to send + * @param len - buf len, or HTTPD_RESP_USE_STRLEN + * @param escape - escaping type + * @return success + */ +esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape); + +/** + * Template substitution callback. Data shall be sent using httpd_resp_send_chunk_escaped(). + * + * @param[in,out] context - user-defined page state data + * @param[in] token - replacement token + * @return ESP_OK if the token was substituted, ESP_ERR_NOT_FOUND if it is unknown, other errors on e.g. send failure + */ +typedef esp_err_t (*template_subst_t)(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape); + +/** + * Send a template file as a response. The content type from the file struct will be used. + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * @param r - request + * @param file_index - file index in EMBEDDED_FILE_LOOKUP + * @param replacer - substitution callback, can be NULL if only includes are to be processed + * @param context - arbitrary context, will be passed to the replacer function; can be NULL + * @param opts - flag options (HTOPT_*) + */ +esp_err_t httpd_send_template_file(httpd_req_t *r, int file_index, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Same as httpd_send_template_file, but using an `embedded_file_info` struct. + */ +esp_err_t httpd_send_template_file_struct(httpd_req_t *r, const struct embedded_file_info *file, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Process and send a string template. + * The content-type header should be set beforehand, if different from the default (text/html). + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * @param r - request + * @param template - template string (does not have to be terminated by a null byte) + * @param template_len - length of the template string; -1 to use strlen() + * @param replacer - substitution callback, can be NULL if only includes are to be processed + * @param context - arbitrary context, will be passed to the replacer function; can be NULL + * @param opts - flag options (HTOPT_*) + */ +esp_err_t httpd_send_template(httpd_req_t *r, const char *template, ssize_t template_len, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Send a static file. This can be used to just send a file, or to embed a static template as a token substitution. + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * Note: use httpd_resp_send_chunk_escaped() or httpd_resp_send_chunk() to send a plain string. + * + * @param r - request + * @param file_index - file index in EMBEDDED_FILE_LOOKUP + * @param escape - escape option + * @param opts - flag options (HTOPT_*) + * @return + */ +esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts); + +/** + * Same as httpd_send_template_file, but using an `embedded_file_info` struct. + */ +esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts); + +struct tpl_kv_entry { + char key[TPL_KV_KEY_LEN]; // copied here + char subst[TPL_KV_SUBST_LEN]; // copied here + char *subst_heap; + SLIST_ENTRY(tpl_kv_entry) link; +}; + +SLIST_HEAD(tpl_kv_list, tpl_kv_entry); + +/** + * key-value replacer that works with a dynamically allocated SLIST. + * + * @param r - request + * @param context - context - must be a pointer to `struct tpl_kv_list` + * @param token - token to replace + * @param escape - escape option + * @return OK/not found/other + */ +esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape); + +/** + * Add a pair into the substitutions list + * + * @param head - list head + * @param key - key, copied + * @param subst - value, copied + * @return success (fails if malloc failed) + */ +esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst); + +/** + * Add a heap-allocated string to the replacer. + * + * @param head - list head + * @param key - key, copied + * @param subst - value, copied + * @return success (fails if malloc failed) + */ +esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst); + +/** + * Convenience function that converts an IP address to string and adds it as a substitution + * + * @param head - list head + * @param key - key, copied + * @param ip4h - host order ipv4 address + * @return success + */ +esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h); + +/** add int as a substitution; key is copied */ +esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num); + +/** add long as a substitution; key is copied */ +esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num); + +/** add printf-formatted value; key is copied */ +esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...) + __attribute__((format(printf,3,4))); + +/** + * Init a substitutions list (on the stack) + * + * @return the list + */ +static inline struct tpl_kv_list tpl_kv_init(void) +{ + return (struct tpl_kv_list) {.slh_first = NULL}; +} + +/** + * Free the list (head is left alone because it was allocated on the stack) + * @param head + */ +void tpl_kv_free(struct tpl_kv_list *head); + +/** + * Send the map as an ASCII table separated by Record Separator (30) and Unit Separator (31). + * Content type is set to application/octet-stream. + * + * key 31 value 30 + * key 31 value 30 + * key 31 value + * + * @param req + */ +esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head); + +#endif //FBNODE_TOKEN_SUBS_H diff --git a/components/fileserver/readme/README.md b/components/fileserver/readme/README.md new file mode 100644 index 0000000..42d6883 --- /dev/null +++ b/components/fileserver/readme/README.md @@ -0,0 +1,29 @@ +Place the `rebuild_file_tables.php` script in a `files` subfolder of the main component. +It requires PHP 7 to run. + +This is what the setup should look like + +``` +main/files/embed/index.html +main/files/rebuild_file_tables.php +main/CMakeLists.txt +main/main.c +``` + +Add this to your CMakeLists.txt before `register_component`: + +``` +#begin staticfiles +#end staticfiles +``` + +The script will update CMakeLists.txt and generate `files_enum.c` and `files_enum.h` when run. + +``` +main/files/files_enum.h +main/files/files_enum.c +``` + +Ensure `files/files_enum.c` is included in the build. + +`www_get_static_file()` is implemented as weak to let you provide custom access authentication logic. diff --git a/components/fileserver/readme/rebuild_file_tables.php b/components/fileserver/readme/rebuild_file_tables.php new file mode 100755 index 0000000..2d81c59 --- /dev/null +++ b/components/fileserver/readme/rebuild_file_tables.php @@ -0,0 +1,170 @@ +#!/usr/bin/env php + 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + 'pem' => 'application/x-pem-file', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ); + + $parts = explode('.', $f); + $suffix = end($parts); + $mime = $mimes[$suffix] ?? 'application/octet-stream'; + + $len = filesize('embed/'.$f); + + $struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},"; + + return + 'extern const uint8_t '.$start.'[];'."\n". + 'extern const uint8_t '.$end.'[];'; +}, $files); + +$externlist = implode("\n", $externs); +$structlist = implode("\n ", $struct_array); + + +file_put_contents('files_enum.h', << +#include + +enum embedded_file_id { + $keylist, + FILE_MAX +}; + +struct embedded_file_info { + const uint8_t * start; + const uint8_t * end; + const char * name; + const char * mime; +}; + +$externlist + +extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[]; + +#endif // _EMBEDDED_FILES_ENUM_H + +FILE +); + +file_put_contents("files_enum.c", << +#include "files_enum.h" + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + $structlist +}; + +FILE +); diff --git a/components/fileserver/src/embedded_files.c b/components/fileserver/src/embedded_files.c new file mode 100644 index 0000000..e61f348 --- /dev/null +++ b/components/fileserver/src/embedded_files.c @@ -0,0 +1,22 @@ +#include +#include "fileserver/embedded_files.h" +#include "string.h" + +esp_err_t __attribute__((weak)) +www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file) +{ + // simple search by name + for(int i = 0; i < EMBEDDED_FILE_LOOKUP_LEN; i++) { + if (0 == strcmp(EMBEDDED_FILE_LOOKUP[i].name, name)) { + *file = &EMBEDDED_FILE_LOOKUP[i]; + return ESP_OK; + } + } + + return ESP_ERR_NOT_FOUND; +} + +bool __attribute__((weak)) +www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access) { + return true; +} diff --git a/components/fileserver/src/token_subs.c b/components/fileserver/src/token_subs.c new file mode 100644 index 0000000..76a8d5e --- /dev/null +++ b/components/fileserver/src/token_subs.c @@ -0,0 +1,619 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#include + +#include "fileserver/embedded_files.h" +#include "fileserver/token_subs.h" + +#define ESP_TRY(x) \ + do { \ + esp_err_t try_er = (x); \ + if (try_er != ESP_OK) return try_er; \ + } while(0) + +static const char* TAG = "token_subs"; + +// TODO implement buffering to avoid sending many tiny chunks when escaping + +/* encode for HTML. returns 0 or 1 - 1 = success */ +static esp_err_t send_html_chunk(httpd_req_t *r, const char *data, ssize_t len) +{ + assert(r); + assert(data); + + int start = 0, end = 0; + char c; + if (len < 0) len = (int) strlen(data); + if (len==0) return ESP_OK; + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; // not return - the last chunk is printed after the loop + } + + if (c == '"' || c == '\'' || c == '<' || c == '>') { + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + start = end + 1; + } + + if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, """, 5)); + else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "'", 5)); + else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "<", 4)); + else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, ">", 4)); + } + + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + return ESP_OK; +} + +/* encode for JS. returns 0 or 1 - 1 = success */ +static esp_err_t send_js_chunk(httpd_req_t *r, const char *data, ssize_t len) +{ + assert(r); + assert(data); + + int start = 0, end = 0; + char c; + if (len < 0) len = (int) strlen(data); + if (len==0) return ESP_OK; + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; // not return - the last chunk is printed after the loop + } + + if (c == '"' || c == '\\' || c == '/' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') { + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + start = end + 1; + } + + if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "\\\"", 2)); + else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "\\'", 2)); + else if (c == '\\') ESP_TRY(httpd_resp_send_chunk(r, "\\\\", 2)); + else if (c == '/') ESP_TRY(httpd_resp_send_chunk(r, "\\/", 2)); + else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "\\u003C", 6)); + else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "\\u003E", 6)); + else if (c == '\n') ESP_TRY(httpd_resp_send_chunk(r, "\\n", 2)); + else if (c == '\r') ESP_TRY(httpd_resp_send_chunk(r, "\\r", 2)); + } + + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + return ESP_OK; +} + + +esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape) +{ + switch (escape) { + default: // this enum should be exhaustive, but in case something went wrong, just print it verbatim + + case TPL_ESCAPE_NONE: + return httpd_resp_send_chunk(r, buf, len); + + case TPL_ESCAPE_HTML: + return send_html_chunk(r, buf, len); + + case TPL_ESCAPE_JS: + return send_js_chunk(r, buf, len); + } +} + +esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts) +{ + assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); + const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; + + return httpd_send_static_file_struct(r, file, escape, opts); +} + +esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts) +{ + if (0 == (opts & HTOPT_NO_HEADERS)) { + ESP_TRY(httpd_resp_set_type(r, file->mime)); + ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "max-age=86400, public, must-revalidate")); + } + + ESP_TRY(httpd_resp_send_chunk_escaped(r, (const char *) file->start, (size_t)(file->end - file->start), escape)); + + if (0 == (opts & HTOPT_NO_CLOSE)) { + ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); + } + + return ESP_OK; +} + +esp_err_t httpd_send_template_file(httpd_req_t *r, + int file_index, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); + const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; + return httpd_send_template_file_struct(r,file,replacer,context,opts); +} + +esp_err_t httpd_send_template_file_struct(httpd_req_t *r, + const struct embedded_file_info *file, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + if (0 == (opts & HTOPT_NO_HEADERS)) { + ESP_TRY(httpd_resp_set_type(r, file->mime)); + ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "no-cache, no-store, must-revalidate")); + } + + return httpd_send_template(r, (const char *) file->start, (size_t)(file->end - file->start), replacer, context, opts); +} + +esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) +{ + assert(context); + assert(token); + + struct tpl_kv_entry *entry; + struct tpl_kv_list *head = context; + SLIST_FOREACH(entry, head, link) { + if (0==strcmp(entry->key, token)) { + return httpd_resp_send_chunk_escaped(r, + entry->subst_heap ? + entry->subst_heap : + entry->subst, + -1, escape); + } + } + + return ESP_ERR_NOT_FOUND; +} + +struct stacked_replacer_context { + template_subst_t replacer0; + void *context0; + template_subst_t replacer1; + void *context1; +}; + +esp_err_t stacked_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) +{ + assert(context); + assert(token); + + struct stacked_replacer_context *combo = context; + + if (ESP_OK == combo->replacer0(r, combo->context0, token, escape)) { + return ESP_OK; + } + + if (ESP_OK == combo->replacer1(r, combo->context1, token, escape)) { + return ESP_OK; + } + + return ESP_ERR_NOT_FOUND; +} + +esp_err_t httpd_send_template(httpd_req_t *r, + const char *template, ssize_t template_len, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + if (template_len < 0) template_len = strlen(template); + + // replacer and context may be NULL + assert(template); + assert(r); + + // data end + const char * const end = template + template_len; + + // start of to-be-processed data + const char * pos = template; + + // start position for finding opening braces, updated after a failed match to avoid infinite loop on the same bad token + const char * searchpos = pos; + + // tokens must be copied to a buffer to allow adding the terminating null byte + char token_buf[MAX_TOKEN_LEN]; + + while (pos < end) { + const char * openbr = strchr(searchpos, '{'); + if (openbr == NULL) { + // no more templates + ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); + break; + } + + // this brace could start a valid template. check if it seems valid... + const char * closebr = strchr(openbr, '}'); + if (closebr == NULL) { + // there are no further closing braces, so it can't be a template + + // we also know there can't be any more substitutions, because they would lack a closing } too + ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); + break; + } + + // see if the braces content looks like a token + const char *t = openbr + 1; + bool token_valid = true; + struct tpl_kv_list substitutions_head = tpl_kv_init(); + struct tpl_kv_entry *new_subst_pair = NULL; + + // a token can be either a name for replacement by the replacer func, or an include with static kv replacements + bool is_include = false; + bool token_is_optional = false; + const char *token_end = NULL; // points one char after the end of the token + + // parsing the token + { + if (*t == '@') { + ESP_LOGD(TAG, "Parsing an Include token"); + is_include = true; + t++; + } + + enum { + P_NAME, P_KEY, P_VALUE + } state = P_NAME; + + const char *kv_start = NULL; + while (t != closebr || state == P_VALUE) { + char c = *t; + + if (state == P_NAME) { + if (!((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-' || c == ':')) { + + if (!is_include && c == '?') { + token_end = t; + token_is_optional = true; + } else { + if (is_include && c == '|') { + token_end = t; + state = P_KEY; + kv_start = t + 1; + // pipe separates the include's filename and literal substitutions + // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct + } + else { + token_valid = false; + break; + } + } + } + } + else if (state == P_KEY) { + if (!((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-')) { + if (c == '=') { + new_subst_pair = calloc(1, sizeof(struct tpl_kv_entry)); + const size_t klen = MIN(TPL_KV_KEY_LEN, t - kv_start); + strncpy(new_subst_pair->key, kv_start, klen); + new_subst_pair->key[klen] = 0; + + kv_start = t + 1; + + state = P_VALUE; + // pipe separates the include's filename and literal substitutions + // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct + } + } + } + else if (state == P_VALUE) { + if (c == '|' || c == '}') { + const size_t vlen = MIN(TPL_KV_SUBST_LEN, t - kv_start); + strncpy(new_subst_pair->subst, kv_start, vlen); + new_subst_pair->subst[vlen] = 0; + + // attach the kv pair to the list + SLIST_INSERT_HEAD(&substitutions_head, new_subst_pair, link); + ESP_LOGD(TAG, "Adding subs kv %s -> %s", new_subst_pair->key, new_subst_pair->subst); + new_subst_pair = NULL; + + kv_start = t + 1; // go past the pipe + state = P_KEY; + + if (t == closebr) { + break; // found the ending brace, so let's quit the kv parse loop + } + } + } + + t++; + } + // clean up after a messed up subs kv pairs syntax + if (new_subst_pair != NULL) { + free(new_subst_pair); + } + } + + if (!token_valid) { + // false match, include it in the block to send before the next token + searchpos = openbr + 1; + ESP_LOGD(TAG, "Skip invalid token near %10s", openbr); + continue; + } + + // now we know it looks like a substitution token + + // flush data before the token + if (pos != openbr) ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (openbr - pos))); + + const char *token_start = openbr; + + tpl_escape_t escape = TPL_ESCAPE_NONE; + + // extract and terminate the token + size_t token_len = MIN(MAX_TOKEN_LEN-1, closebr - openbr - 1); + if (token_end) { + token_len = MIN(token_len, token_end - openbr - 1); + } + + if (is_include) { + token_start += 1; // skip the @ + token_len -= 1; + } else { + if (0 == strncmp("h:", openbr + 1, 2)) { + escape = TPL_ESCAPE_HTML; + token_start += 2; + token_len -= 2; + } + else if (0 == strncmp("j:", openbr + 1, 2)) { + escape = TPL_ESCAPE_JS; + token_start += 2; + token_len -= 2; + } + } + + strncpy(token_buf, token_start+1, token_len); + token_buf[token_len] = 0; + + ESP_LOGD(TAG, "Token: %s", token_buf); + + esp_err_t rv; + + if (is_include) { + ESP_LOGD(TAG, "Trying to include a sub-file"); + + const struct embedded_file_info *file = NULL; + rv = www_get_static_file(token_buf, FILE_ACCESS_PROTECTED, &file); + if (rv != ESP_OK) { + ESP_LOGE(TAG, "Failed to statically include \"%s\" in a template - %s", token_buf, esp_err_to_name(rv)); + // this will cause the token to be emitted verbatim + } else { + ESP_LOGD(TAG, "Descending..."); + + // combine the two replacers + struct stacked_replacer_context combo = { + .replacer0 = replacer, + .context0 = context, + .replacer1 = tpl_kv_replacer, + .context1 = &substitutions_head + }; + + rv = httpd_send_template_file_struct(r, file, stacked_replacer, &combo, HTOPT_INCLUDE); + ESP_LOGD(TAG, "...back in parent"); + } + + // tear down the list + tpl_kv_free(&substitutions_head); + + if (rv != ESP_OK) { + // just send it verbatim... + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } else { + if (replacer) { + ESP_LOGD(TAG, "Running replacer for \"%s\" with escape %d", token_buf, escape); + rv = replacer(r, context, token_buf, escape); + + if (rv != ESP_OK) { + if (rv == ESP_ERR_NOT_FOUND) { + ESP_LOGD(TAG, "Token rejected"); + // optional token becomes empty string if not replaced + if (!token_is_optional) { + ESP_LOGD(TAG, "Not optional, keeping verbatim"); + // replacer rejected the token, keep it verbatim + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } + else { + ESP_LOGE(TAG, "Unexpected error from replacer func: 0x%02x - %s", rv, esp_err_to_name(rv)); + return rv; + } + } + } else { + ESP_LOGD(TAG, "Not replacer"); + // no replacer, only includes - used for 'static' files + if (!token_is_optional) { + ESP_LOGD(TAG, "Token not optional, keeping verbatim"); + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } + } + + searchpos = pos = closebr + 1; + } + + if (0 == (opts & HTOPT_NO_CLOSE)) { + ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); + } + + return ESP_OK; +} + + +esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num) +{ + char buf[12]; + itoa(num, buf, 10); + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num) +{ + char buf[21]; + sprintf(buf, "%"PRIi64, num); + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h) +{ + char buf[IP4ADDR_STRLEN_MAX]; + ip4_addr_t addr; + addr.addr = lwip_htonl(ip4h); + ip4addr_ntoa_r(&addr, buf, IP4ADDR_STRLEN_MAX); + + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst) +{ + ESP_LOGD(TAG, "kv add subs %s := %s", key, subst); + struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); + if (entry == NULL) return ESP_ERR_NO_MEM; + + assert(strlen(key) < TPL_KV_KEY_LEN); + assert(strlen(subst) < TPL_KV_SUBST_LEN); + + strncpy(entry->key, key, TPL_KV_KEY_LEN); + entry->key[TPL_KV_KEY_LEN - 1] = 0; + + strncpy(entry->subst, subst, TPL_KV_SUBST_LEN - 1); + entry->subst[TPL_KV_KEY_LEN - 1] = 0; + + SLIST_INSERT_HEAD(head, entry, link); + return ESP_OK; +} + +esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst) +{ + ESP_LOGD(TAG, "kv add subs %s := (heap str)", key); + struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); + if (entry == NULL) return ESP_ERR_NO_MEM; + + assert(strlen(key) < TPL_KV_KEY_LEN); + + strncpy(entry->key, key, TPL_KV_KEY_LEN); + entry->key[TPL_KV_KEY_LEN - 1] = 0; + + entry->subst_heap = subst; + + SLIST_INSERT_HEAD(head, entry, link); + return ESP_OK; +} + +esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...) +{ + ESP_LOGD(TAG, "kv printf %s := %s", key, format); + struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); + if (entry == NULL) return ESP_ERR_NO_MEM; + + assert(strlen(key) < TPL_KV_KEY_LEN); + + strncpy(entry->key, key, TPL_KV_KEY_LEN); + entry->key[TPL_KV_KEY_LEN - 1] = 0; + + va_list list; + va_start(list, format); + vsnprintf(entry->subst, TPL_KV_SUBST_LEN, format, list); + va_end(list); + entry->subst[TPL_KV_KEY_LEN - 1] = 0; + + SLIST_INSERT_HEAD(head, entry, link); + return ESP_OK; +} + +void tpl_kv_free(struct tpl_kv_list *head) +{ + struct tpl_kv_entry *item, *next; + SLIST_FOREACH_SAFE(item, head, link, next) { + if (item->subst_heap) { + free(item->subst_heap); + item->subst_heap = NULL; + } + + free(item); + } +} + +esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head) +{ + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + +#define BUF_CAP 512 + char *buf_head = malloc(BUF_CAP); + if (!buf_head) { + ESP_LOGE(TAG, "Malloc err"); + return ESP_FAIL; + } + char *buf = buf_head; + size_t cap = BUF_CAP; + struct tpl_kv_entry *entry; + + // GCC nested function + esp_err_t send_part() { + esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); + buf = buf_head; // buf is assigned to buf head + cap = BUF_CAP; + if (suc != ESP_OK) { + ESP_LOGE(TAG, "Error sending buffer"); + free(buf_head); + httpd_resp_send_chunk(req, NULL, 0); + } + return suc; + } + + SLIST_FOREACH(entry, head, link) { + while(NULL == (buf = append(buf, entry->key, &cap))) ESP_TRY(send_part()); + while(NULL == (buf = append(buf, "\x1f", &cap))) ESP_TRY(send_part()); + + if (entry->subst_heap) { + if (strlen(entry->subst_heap) >= BUF_CAP) { + // send what we have + ESP_TRY(send_part()); + esp_err_t suc = httpd_resp_send_chunk(req, entry->subst_heap, -1); + if (suc != ESP_OK) { + ESP_LOGE(TAG, "Error sending buffer"); + free(buf_head); + httpd_resp_send_chunk(req, NULL, 0); + } + } else { + while (NULL == (buf = append(buf, entry->subst_heap, &cap))) ESP_TRY(send_part()); + } + } else { + while(NULL == (buf = append(buf, entry->subst, &cap))) ESP_TRY(send_part()); + } + + if (entry->link.sle_next) { + while(NULL == (buf = append(buf, "\x1e", &cap))) ESP_TRY(send_part()); + } + } + // send leftovers + if (buf != buf_head) { + esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); + if (suc != ESP_OK) { + ESP_LOGE(TAG, "Error sending buffer"); + } + } + + // Commit + httpd_resp_send_chunk(req, NULL, 0); + free(buf_head); + return ESP_OK; +} diff --git a/components/httpd_utils/CMakeLists.txt b/components/httpd_utils/CMakeLists.txt new file mode 100644 index 0000000..e5fe530 --- /dev/null +++ b/components/httpd_utils/CMakeLists.txt @@ -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() diff --git a/components/httpd_utils/README.txt b/components/httpd_utils/README.txt new file mode 100644 index 0000000..c3e034e --- /dev/null +++ b/components/httpd_utils/README.txt @@ -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. diff --git a/components/httpd_utils/component.mk b/components/httpd_utils/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/httpd_utils/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/httpd_utils/include/httpd_utils/captive.h b/components/httpd_utils/include/httpd_utils/captive.h new file mode 100644 index 0000000..d656b41 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/captive.h @@ -0,0 +1,32 @@ +#ifndef HTTPD_UTILS_CAPTIVE_H +#define HTTPD_UTILS_CAPTIVE_H + +#include +#include + +/** + * 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 diff --git a/components/httpd_utils/include/httpd_utils/fd_to_ipv4.h b/components/httpd_utils/include/httpd_utils/fd_to_ipv4.h new file mode 100644 index 0000000..6cb647d --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/fd_to_ipv4.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 diff --git a/components/httpd_utils/include/httpd_utils/redirect.h b/components/httpd_utils/include/httpd_utils/redirect.h new file mode 100644 index 0000000..77fdebc --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/redirect.h @@ -0,0 +1,16 @@ +#ifndef HTTPD_UTILS_REDIRECT_H +#define HTTPD_UTILS_REDIRECT_H + +#include +#include + +/** + * 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 diff --git a/components/httpd_utils/include/httpd_utils/session.h b/components/httpd_utils/include/httpd_utils/session.h new file mode 100644 index 0000000..1ff409b --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session.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 diff --git a/components/httpd_utils/include/httpd_utils/session_kvmap.h b/components/httpd_utils/include/httpd_utils/session_kvmap.h new file mode 100644 index 0000000..e091bd5 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session_kvmap.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 diff --git a/components/httpd_utils/include/httpd_utils/session_store.h b/components/httpd_utils/include/httpd_utils/session_store.h new file mode 100644 index 0000000..2266944 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session_store.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 diff --git a/components/httpd_utils/src/captive.c b/components/httpd_utils/src/captive.c new file mode 100644 index 0000000..2be20d3 --- /dev/null +++ b/components/httpd_utils/src/captive.c @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include +#include + +#include "httpd_utils/captive.h" +#include "httpd_utils/fd_to_ipv4.h" +#include "httpd_utils/redirect.h" +#include + +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; +} diff --git a/components/httpd_utils/src/fd_to_ipv4.c b/components/httpd_utils/src/fd_to_ipv4.c new file mode 100644 index 0000000..7f6377f --- /dev/null +++ b/components/httpd_utils/src/fd_to_ipv4.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include + +#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; +} diff --git a/components/httpd_utils/src/redirect.c b/components/httpd_utils/src/redirect.c new file mode 100644 index 0000000..5a174df --- /dev/null +++ b/components/httpd_utils/src/redirect.c @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include + +#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); +} diff --git a/components/httpd_utils/src/session_kvmap.c b/components/httpd_utils/src/session_kvmap.c new file mode 100644 index 0000000..e08af41 --- /dev/null +++ b/components/httpd_utils/src/session_kvmap.c @@ -0,0 +1,181 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/components/httpd_utils/src/session_store.c b/components/httpd_utils/src/session_store.c new file mode 100644 index 0000000..94eb167 --- /dev/null +++ b/components/httpd_utils/src/session_store.c @@ -0,0 +1,220 @@ +//#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 = 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); +} diff --git a/components/httpd_utils/src/session_utils.c b/components/httpd_utils/src/session_utils.c new file mode 100644 index 0000000..c1ef068 --- /dev/null +++ b/components/httpd_utils/src/session_utils.c @@ -0,0 +1,41 @@ +/** + * TODO file description + * + * Created on 2019/07/13. + */ + +#ifndef SESSION_UTILS_C_H +#define SESSION_UTILS_C_H + +#include +#include +#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 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ae6cf28..f6d21f1 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -16,7 +16,17 @@ set(COMPONENT_SRCS "graphics/font.c" "graphics/drawing.c" "graphics/bitmaps.c" + "web/websrv.c" + "files/files_enum.c" + "utils.c" ) set(COMPONENT_ADD_INCLUDEDIRS "." "liquid" "graphics") +#begin staticfiles +# generated by rebuild_file_tables +set(COMPONENT_EMBED_FILES + "files/embed/favicon.ico" + "files/embed/index.html") +#end staticfiles + register_component() diff --git a/main/analog.c b/main/analog.c index e6ae5d9..43aedd6 100644 --- a/main/analog.c +++ b/main/analog.c @@ -16,6 +16,13 @@ static float measurement_celsius; static const adc_atten_t atten = ADC_ATTEN_DB_0; static const adc_unit_t unit = ADC_UNIT_1; +float reg_meas_history[REG_HISTORY_LEN] = {}; +float reg_tset_history[REG_HISTORY_LEN] = {}; +uint32_t history_counter = 0; + +// TODO move to regulator module (make extern) +float reg_setpoint = 125; + static void analog_service(void *arg); static TaskHandle_t hAnalog; @@ -63,7 +70,15 @@ static void __attribute__((noreturn)) analog_service(void *arg) { measurement_celsius = celsius; - vTaskDelay(pdMS_TO_TICKS(100)); + for (int i = 0; i < REG_HISTORY_LEN-1; i++) { + reg_meas_history[i] = reg_meas_history[i+1]; + reg_tset_history[i] = reg_tset_history[i+1]; + } + reg_meas_history[REG_HISTORY_LEN-1] = celsius; + reg_tset_history[REG_HISTORY_LEN-1] = reg_setpoint; + history_counter = (history_counter + 1) % 20; + + vTaskDelay(pdMS_TO_TICKS(500)); } } diff --git a/main/analog.h b/main/analog.h index 875ec16..b4b746e 100644 --- a/main/analog.h +++ b/main/analog.h @@ -7,6 +7,11 @@ #ifndef REFLOWER_ANALOG_H #define REFLOWER_ANALOG_H +#define REG_HISTORY_LEN 121 +extern float reg_meas_history[REG_HISTORY_LEN]; +extern float reg_tset_history[REG_HISTORY_LEN]; +extern uint32_t history_counter; + void analog_init(); float analog_read(); diff --git a/main/app_main.c b/main/app_main.c index f2d0be8..055f2e7 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -7,6 +7,8 @@ CONDITIONS OF ANY KIND, either express or implied. */ #include +#include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" @@ -15,11 +17,68 @@ #include "knob.h" #include "gui.h" #include "analog.h" +#include "utils.h" +#include "esp_event_loop.h" +#include "nvs_flash.h" + +/** + * Application event handler + * + * @param ctx - ignored, context passed to esp_event_loop_init() + * @param event + * @return success + */ +static esp_err_t event_handler(void *ctx, system_event_t *event) +{ + switch (event->event_id) { + case SYSTEM_EVENT_STA_START: + try_reconn_if_have_wifi_creds(); + break; + + case SYSTEM_EVENT_STA_CONNECTED: + // we should get an IP address soon + break; + + case SYSTEM_EVENT_STA_GOT_IP: +// xEventGroupSetBits(g_wifi_event_group, EG_WIFI_CONNECTED_BIT); + break; + + case SYSTEM_EVENT_STA_DISCONNECTED: +// xEventGroupClearBits(g_wifi_event_group, EG_WIFI_CONNECTED_BIT); + try_reconn_if_have_wifi_creds(); + break; + + default: + break; + } + +// dhcp_watchdog_notify(dhcp_wd, event->event_id); + + return ESP_OK; +} + + +/** + * Initialize WiFi in station mode, try to connect if settings are stored. + * Set up WiFi event group & start related tasks + */ +static void initialise_wifi(void) +{ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + ESP_ERROR_CHECK(esp_wifi_start()); +} void __attribute__((noreturn)) app_main() { - printf("Hello world!\n"); + ESP_ERROR_CHECK(nvs_flash_init()); + //ESP_ERROR_CHECK(esp_register_shutdown_handler(cspemu_run_shutdown_handlers)); + + ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); gpio_config_t output = { .pin_bit_mask = (1<<22), @@ -27,11 +86,29 @@ void __attribute__((noreturn)) app_main() }; gpio_config(&output); + tcpip_adapter_init(); + initialise_wifi(); + + // TODO add proper join procedure + const char *ssid = "Chlivek_2.4G"; + const char *pass = "slepice123"; + wifi_config_t wifi_config = {}; + strncpy((char*) wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid)); + if (pass) { + strncpy((char*) wifi_config.sta.password, pass, sizeof(wifi_config.sta.password)); + } + ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); + ESP_ERROR_CHECK( esp_wifi_disconnect() ); + ESP_ERROR_CHECK( esp_wifi_connect() ); gui_init(); knob_init(); analog_init(); + try_reconn_if_have_wifi_creds(); + websrv_init(); + bool level = 0; while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); diff --git a/main/files/embed/chart.ignore.svg b/main/files/embed/chart.ignore.svg new file mode 100644 index 0000000..9bc36eb --- /dev/null +++ b/main/files/embed/chart.ignore.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + 400 °C + 300 °C + 200 °C + 100 °C + 0 °C + + + diff --git a/main/files/embed/favicon.ico b/main/files/embed/favicon.ico new file mode 100644 index 0000000..342529e Binary files /dev/null and b/main/files/embed/favicon.ico differ diff --git a/main/files/embed/index.html b/main/files/embed/index.html new file mode 100644 index 0000000..98f2a13 --- /dev/null +++ b/main/files/embed/index.html @@ -0,0 +1,137 @@ + + + + +Breadflow Web Control + + + +

Breadflow {version}

+ + + + + + +
+ + + + + + + + + + + + + + 400 °C + 300 °C + 200 °C + 100 °C + 0 °C + + + + +Sidebar +
+ + diff --git a/main/files/files_enum.c b/main/files/files_enum.c new file mode 100644 index 0000000..f2a18a9 --- /dev/null +++ b/main/files/files_enum.c @@ -0,0 +1,15 @@ +// Generated by 'rebuild_file_tables' +#include +#include "files_enum.h" + +extern const uint8_t _binary_favicon_ico_start[]; +extern const uint8_t _binary_favicon_ico_end[]; +extern const uint8_t _binary_index_html_start[]; +extern const uint8_t _binary_index_html_end[]; + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + [FILE_FAVICON_ICO] = {_binary_favicon_ico_start, _binary_favicon_ico_end, "favicon.ico", "image/vnd.microsoft.icon"}, + [FILE_INDEX_HTML] = {_binary_index_html_start, _binary_index_html_end, "index.html", "text/html"}, +}; + +const size_t EMBEDDED_FILE_LOOKUP_LEN = 2; diff --git a/main/files/files_enum.h b/main/files/files_enum.h new file mode 100644 index 0000000..b81113b --- /dev/null +++ b/main/files/files_enum.h @@ -0,0 +1,13 @@ +// Generated by 'rebuild_file_tables' + +#ifndef _EMBEDDED_FILES_ENUM_H +#define _EMBEDDED_FILES_ENUM_H + +#include "fileserver/embedded_files.h" + +enum embedded_file_id { + FILE_FAVICON_ICO = 0, + FILE_INDEX_HTML = 1 +}; + +#endif // _EMBEDDED_FILES_ENUM_H diff --git a/main/files/rebuild_file_tables.php b/main/files/rebuild_file_tables.php new file mode 100755 index 0000000..3d78368 --- /dev/null +++ b/main/files/rebuild_file_tables.php @@ -0,0 +1,163 @@ +#!/usr/bin/env php + 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + 'pem' => 'application/x-pem-file', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ); + + $parts = explode('.', $f); + $suffix = end($parts); + $mime = $mimes[$suffix] ?? 'application/octet-stream'; + + $len = filesize('embed/'.$f); + + $struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},"; + + return + 'extern const uint8_t '.$start.'[];'."\n". + 'extern const uint8_t '.$end.'[];'; +}, $files); + +$externlist = implode("\n", $externs); +$structlist = implode("\n ", $struct_array); + + +file_put_contents('files_enum.h', << +#include "files_enum.h" + +$externlist + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + $structlist +}; + +const size_t EMBEDDED_FILE_LOOKUP_LEN = $files_count; + +FILE +); diff --git a/main/utils.c b/main/utils.c new file mode 100644 index 0000000..91ea280 --- /dev/null +++ b/main/utils.c @@ -0,0 +1,70 @@ +#include +#include +#include "esp_log.h" +#include "esp_wifi.h" +#include "utils.h" + +static const char * TAG = "utils.c"; + +static void recon_internal(void); + +/** + * Attempt to reconnect to AP if we have SSID stored + */ +void try_reconn_if_have_wifi_creds(void) +{ + recon_internal(); +} + +static volatile bool recurse = false; + +static void recon_internal(void) +{ + if (recurse) { + ESP_LOGE(TAG, "Stopping recursion!"); + } + recurse = true; + + wifi_config_t wificonf; + + // try to connect if we have a saved config + ESP_ERROR_CHECK(esp_wifi_get_config(WIFI_IF_STA, &wificonf)); + + if (wificonf.sta.ssid[0]) { + ESP_LOGI(TAG, "(Re)connecting using saved STA creds"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } else { + ESP_LOGI(TAG, "No WiFi creds, no (re)conn"); + } + + recurse = false; +} + + +esp_err_t nvs_get_bool(nvs_handle handle, const char* key, bool* out_value) +{ + uint8_t x = (uint8_t) *out_value; + esp_err_t rv = nvs_get_u8(handle, key, &x); + if (rv == ESP_OK) { + *out_value = (bool)x; + } + return rv; +} + +esp_err_t nvs_set_bool(nvs_handle handle, const char* key, bool value) +{ + return nvs_set_u8(handle, key, (uint8_t) value); +} + +void feed_all_dogs(void) +{ + TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_feed=1; + TIMERG0.wdt_wprotect=0; + + TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config2=CONFIG_INT_WDT_TIMEOUT_MS*2; //Set timeout before interrupt + TIMERG1.wdt_config3=CONFIG_INT_WDT_TIMEOUT_MS*4; //Set timeout before reset + TIMERG1.wdt_feed=1; + TIMERG1.wdt_wprotect=0; +} diff --git a/main/utils.h b/main/utils.h new file mode 100644 index 0000000..0ce39ce --- /dev/null +++ b/main/utils.h @@ -0,0 +1,76 @@ +/** + * Utilities and helpers + */ + +#ifndef CSPEMU_UTILS_H +#define CSPEMU_UTILS_H + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include +#include + +/** Error check macro for FreeRTOS status codes */ +#define RTOS_ERROR_CHECK(code) if (pdPASS != (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** Error check macro for NULL return value */ +#define NULL_CHECK(code) if (NULL == (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** Error check macro for CSP status codes */ +#define CSP_ERROR_CHECK(code) if (CSP_ERR_NONE != (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** + * Reconnect to WiFi if there are saved credentials. + * Called from the main event group handler. + */ +void try_reconn_if_have_wifi_creds(void); + +/** + * Helper to retrieve a bool value from the NVS. Internally uses 'u8' + * + * @param handle - NVS storage + * @param key + * @param out_value + * @return success + */ +esp_err_t nvs_get_bool(nvs_handle handle, const char* key, bool* out_value); + +/** + * Helper to store a bool into the NVS. Internally uses 'u8' + * + * @param handle - NVS storage + * @param key + * @param value + * @return success + */ +esp_err_t nvs_set_bool(nvs_handle handle, const char* key, bool value); + +/** + * Feed watchdogs (inside a busy wait loop) + */ +void feed_all_dogs(void); + +/** + * @brief malloc() and snprintf() combined + * @attention DO NOT use in if/for/do etc without braces. + * + * The caller is responsible for disposing of the allocated string afterwards. + */ +#define malloc_sprintf(var, n, format, ...) \ + char *var = malloc(n); \ + if (!var) assert(0); \ + snprintf(var, n, format, ##__VA_ARGS__); + +/** + * Generate externs for an embedded file. + * Variables {varname} and {varname}_end will be produced. + */ +#define efile(varname, filename) \ + extern const char varname[] asm("_binary_"filename"_start"); \ + extern const char varname##_end[] asm("_binary_"filename"_end"); + +/** Get embedded file size (must be declared with efile() first) */ +#define efsize(varname) (varname##_end - varname) + +#endif //CSPEMU_UTILS_H diff --git a/main/version.h b/main/version.h new file mode 100644 index 0000000..8fe7e6a --- /dev/null +++ b/main/version.h @@ -0,0 +1,12 @@ +/** + * TODO file description + * + * Created on 2020/01/06. + */ + +#ifndef REFLOWER_VERSION_H +#define REFLOWER_VERSION_H + +#define APP_VERSION "0.1" + +#endif //REFLOWER_VERSION_H diff --git a/main/web/websrv.c b/main/web/websrv.c new file mode 100644 index 0000000..2e506f6 --- /dev/null +++ b/main/web/websrv.c @@ -0,0 +1,212 @@ +#include +#include + +#include +#include + +#include "websrv.h" +#include "esp_http_server.h" +#include "utils.h" +#include "files/files_enum.h" +#include "version.h" +#include "analog.h" + +static const char *TAG="websrv"; + +static httpd_handle_t s_hServer = NULL; + +// Embedded files (must also be listed in CMakeLists.txt as COMPONENT_EMBED_TXTFILES) +efile(index_file, "index_html"); + +static struct tpl_kv_list build_index_replacements_kv(void) +{ +// char name[TPL_KV_KEY_LEN]; + struct tpl_kv_list kv = tpl_kv_init(); + tpl_kv_add(&kv, "version", APP_VERSION); + + size_t pcap1 = 300; + size_t pcap2 = 300; + char *path1 = malloc(pcap1); + char *path2 = malloc(pcap2); + assert(path1); + assert(path2); + path1[0] = 0; + path2[0] = 0; + +#define SCRATCH_SIZE 15 + char scratch1[SCRATCH_SIZE]; + char scratch2[SCRATCH_SIZE]; + + bool last_empty = true; // first is move + bool suc; + for (int i = 0; i < REG_HISTORY_LEN; i++) { + int x = i*5; + if (reg_meas_history[i] == 0) { + snprintf(scratch1, SCRATCH_SIZE, "M%d,0", x); + snprintf(scratch2, SCRATCH_SIZE, "M%d,0", x); + last_empty = true; + } else { + snprintf(scratch1, SCRATCH_SIZE, "%c%d,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_meas_history[i])); + snprintf(scratch2, SCRATCH_SIZE, "%c%d,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_tset_history[i])); + last_empty = false; + } + + suc = append_realloc(&path1, &pcap1, scratch1); + assert(suc); + + suc = append_realloc(&path2, &pcap2, scratch2); + assert(suc); + } + tpl_kv_add_heapstr(&kv, "ser-act", path1); + tpl_kv_add_heapstr(&kv, "ser-set", path2); + + tpl_kv_add_int(&kv, "timeshift", history_counter); + +#undef SCRATCH_SIZE + + return kv; +} + +/* Main page */ +static esp_err_t handler_index(httpd_req_t *req) +{ + struct tpl_kv_list kv = build_index_replacements_kv(); + + esp_err_t suc = httpd_send_template_file(req, FILE_INDEX_HTML, tpl_kv_replacer, &kv, 0); + tpl_kv_free(&kv); + return suc; +} + +/* Update XHR for new index page data */ +static esp_err_t handler_update(httpd_req_t *req) +{ + struct tpl_kv_list kv = build_index_replacements_kv(); + + esp_err_t suc = tpl_kv_send_as_ascii_map(req, &kv); + tpl_kv_free(&kv); + return suc; +} + +/* Set a param */ +static esp_err_t handler_set(httpd_req_t *req) +{ + char buf[64]; + int n = httpd_req_recv(req, buf, 63); + if (n < 0) { + ESP_LOGW(TAG, "rx er"); + goto err; + } + buf[n]=0; + + char keybuf[20]; + char valbuf[20]; + if (ESP_OK != httpd_query_key_value(buf, "key", keybuf, 20)) goto err; + if (ESP_OK != httpd_query_key_value(buf, "value", valbuf, 20)) goto err; + + // TODO handle + + return httpd_resp_send(req, NULL, 0); + +err: + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); +} + +/* Request emulator reboot */ +static esp_err_t handler_reboot(httpd_req_t *req) +{ + httpd_resp_send(req, "" + "" + "" + "" + "Reboot requested. Reloading in 10 seconds.
" + "Try now.", -1); + + ESP_LOGI(TAG, "Restarting ESP..."); + esp_restart(); +} + +/* An HTTP GET handler */ +static esp_err_t handler_staticfiles(httpd_req_t *r) +{ + const struct embedded_file_info *file; + const char *fname = r->user_ctx; + enum file_access_level access = FILE_ACCESS_PROTECTED; + + // wildcard files must be public + if (fname == NULL) { + fname = r->uri + 1; // URI always starts with slash, but we dont want a slash in the file name + access = FILE_ACCESS_PUBLIC; + } + +#ifdef USE_CAPTIVE_PORTAL + // First check if this is a phone taken here by the captive portal + esp_err_t rv = httpd_captive_redirect(r); + if (rv != ESP_ERR_NOT_FOUND) return rv; +#endif + + if (ESP_OK != www_get_static_file(fname, access, &file)) { + ESP_LOGW(TAG, "File not found: %s", fname); + return httpd_resp_send_404(r); + } + else { + if (streq(file->mime, "text/html")) { + // using the template func to allow includes + return httpd_send_template_file_struct(r, file, /*replacer*/NULL, /*ctx*/NULL, /*opts*/0); + } else { + return httpd_send_static_file_struct(r, file, TPL_ESCAPE_NONE, 0); + } + } +} + +static const httpd_uri_t routes[] = { + { + .uri = "/", + .method = HTTP_GET, + .handler = handler_index, + }, + { + .uri = "/data", + .method = HTTP_GET, + .handler = handler_update, + }, + { + .uri = "/set", + .method = HTTP_POST, + .handler = handler_set, + }, + { + .uri = "/reboot", + .method = HTTP_GET, + .handler = handler_reboot, + }, + { + .uri = "*", // any file except protected (e.g. not HTML, PEM etc) + .method = HTTP_GET, + .handler = handler_staticfiles, + }, +}; + +esp_err_t websrv_init(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = 6000; + + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = true; + + ESP_LOGI(TAG, "Starting HTTP server on port: '%d'", config.server_port); + + esp_err_t suc = httpd_start(&s_hServer, &config); + if (suc == ESP_OK) { + ESP_LOGI(TAG, "HTTP server started"); + + for (int i = 0; i < sizeof(routes) / sizeof(httpd_uri_t); i++) { + httpd_register_uri_handler(s_hServer, &routes[i]); + } + + return ESP_OK; + } + + ESP_LOGE(TAG, "Error starting server!"); + return suc; +} diff --git a/main/web/websrv.h b/main/web/websrv.h new file mode 100644 index 0000000..3bc9679 --- /dev/null +++ b/main/web/websrv.h @@ -0,0 +1,14 @@ +/** + * Integrated webserver + * + * Created on 2019/07/13. + */ + +#ifndef CSPEMU_WEBSRV_H +#define CSPEMU_WEBSRV_H + +#include + +esp_err_t websrv_init(void); + +#endif //CSPEMU_WEBSRV_H