Compare commits

...

2 Commits

  1. 8
      components/common_utils/CMakeLists.txt
  2. 2
      components/common_utils/README.txt
  3. 3
      components/common_utils/component.mk
  4. 75
      components/common_utils/include/common_utils/base16.h
  5. 131
      components/common_utils/include/common_utils/datetime.h
  6. 19
      components/common_utils/include/common_utils/hexdump.h
  7. 101
      components/common_utils/include/common_utils/utils.h
  8. 62
      components/common_utils/src/base16.c
  9. 85
      components/common_utils/src/common_utils.c
  10. 110
      components/common_utils/src/datetime.c
  11. 72
      components/common_utils/src/hexdump.c
  12. 9
      components/fileserver/CMakeLists.txt
  13. 2
      components/fileserver/README.txt
  14. 3
      components/fileserver/component.mk
  15. 51
      components/fileserver/include/fileserver/embedded_files.h
  16. 227
      components/fileserver/include/fileserver/token_subs.h
  17. 29
      components/fileserver/readme/README.md
  18. 170
      components/fileserver/readme/rebuild_file_tables.php
  19. 22
      components/fileserver/src/embedded_files.c
  20. 619
      components/fileserver/src/token_subs.c
  21. 9
      components/httpd_utils/CMakeLists.txt
  22. 4
      components/httpd_utils/README.txt
  23. 3
      components/httpd_utils/component.mk
  24. 32
      components/httpd_utils/include/httpd_utils/captive.h
  25. 13
      components/httpd_utils/include/httpd_utils/fd_to_ipv4.h
  26. 16
      components/httpd_utils/include/httpd_utils/redirect.h
  27. 55
      components/httpd_utils/include/httpd_utils/session.h
  28. 77
      components/httpd_utils/include/httpd_utils/session_kvmap.h
  29. 100
      components/httpd_utils/include/httpd_utils/session_store.h
  30. 106
      components/httpd_utils/src/captive.c
  31. 42
      components/httpd_utils/src/fd_to_ipv4.c
  32. 20
      components/httpd_utils/src/redirect.c
  33. 181
      components/httpd_utils/src/session_kvmap.c
  34. 220
      components/httpd_utils/src/session_store.c
  35. 41
      components/httpd_utils/src/session_utils.c
  36. 10
      main/CMakeLists.txt
  37. 17
      main/analog.c
  38. 5
      main/analog.h
  39. 79
      main/app_main.c
  40. 54
      main/files/embed/chart.ignore.svg
  41. BIN
      main/files/embed/favicon.ico
  42. 137
      main/files/embed/index.html
  43. 15
      main/files/files_enum.c
  44. 13
      main/files/files_enum.h
  45. 163
      main/files/rebuild_file_tables.php
  46. 2
      main/scenes/scene_bootanim.c
  47. 9
      main/scenes/scene_menu.c
  48. 30
      main/scenes/scene_menu.h
  49. 5
      main/scenes/scene_number.c
  50. 11
      main/scenes/scene_number.h
  51. 21
      main/scenes/scene_test_menu.c
  52. 70
      main/utils.c
  53. 76
      main/utils.h
  54. 12
      main/version.h
  55. 212
      main/web/websrv.c
  56. 14
      main/web/websrv.h

@ -0,0 +1,8 @@
set(COMPONENT_ADD_INCLUDEDIRS include)
set(COMPONENT_SRCDIRS
"src")
#set(COMPONENT_REQUIRES)
register_component()

@ -0,0 +1,2 @@
General purpose, mostly platofrm-idependent utilities
that may be used by other components.

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,75 @@
/*
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
*
* 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 <stdint.h>
#include <string.h>
/**
* 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_ */

@ -0,0 +1,131 @@
/**
* TODO file description
*
* Created on 2019/09/13.
*/
#ifndef CSPEMU_DATETIME_H
#define CSPEMU_DATETIME_H
#include <stdbool.h>
#include <stdint.h>
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

@ -0,0 +1,19 @@
/**
* @file
* @brief A simple way of dumping memory to a hex output
*
* \addtogroup Hexdump
*
* @{
*/
#include <stdio.h>
#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);
/**
* }@
*/

@ -0,0 +1,101 @@
/**
* General purpose, platform agnostic, reusable utils
*/
#ifndef COMMON_UTILS_UTILS_H
#define COMMON_UTILS_UTILS_H
#include <stdbool.h>
#include <stdint.h>
#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

@ -0,0 +1,62 @@
/*
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
*
* 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 <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <esp_log.h>
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);
}

@ -0,0 +1,85 @@
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>
#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;
}

@ -0,0 +1,110 @@
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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;
}

@ -0,0 +1,72 @@
/*
* util.c
*
* Created on: Aug 12, 2009
* Author: johan
*/
// adapted from libgomspace
#include <string.h>
#include <stdio.h>
#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<len; i++) {
j++;
fprintf(fp, "%02X ", ((volatile unsigned char *)src)[i]);
if(j == 8)
fputc(' ', fp);
if(j == 16) {
j = 0;
memcpy(text, &((char *)src)[i-15], 16);
for(k=0; k<16; k++) {
if((text[k] < 32) || (text[k] > 126)) {
text[k] = '.';
}
}
fprintf(fp, " |%s|\n\r", text);
if(i<len-1) {
fprintf(fp, "%p : ", src+i+1);
}
}
}
if (i % 16)
fprintf(fp, "\r\n");
}
void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len)
{
unsigned i;
fprintf(fp, "%0*x", addr_size, pos);
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ; i++)
{
if (!(i % 8))
fputc(' ', fp);
if (i < len)
fprintf(fp, " %02x", (unsigned char)line[i]);
else
fputs(" ", fp);
}
fputs(" |", fp);
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ && i < len; i++)
{
if (line[i] >= 32 && line[i] <= 126)
fprintf(fp, "%c", (unsigned char)line[i]);
else
fputc('.', fp);
}
fputs("|\r\n", fp);
}

@ -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()

@ -0,0 +1,2 @@
File and template serving support for the http_server bundled with ESP-IDF.

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,51 @@
//
// Created on 2018/10/17 by Ondrej Hruska <ondra@ondrovo.com>
//
#ifndef FBNODE_EMBEDDED_FILES_H
#define FBNODE_EMBEDDED_FILES_H
#include <stdint.h>
#include <esp_err.h>
#include <stdbool.h>
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

@ -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}";
// <input value="{h:old_value}">
// {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.
//
// <input value="{h:old_value?}">
//
// 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 <ondra@ondrovo.com>
//
#ifndef FBNODE_TOKEN_SUBS_H
#define FBNODE_TOKEN_SUBS_H
#include "embedded_files.h"
#include <rom/queue.h>
#include <esp_err.h>
#include <esp_http_server.h>
/** 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

@ -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.

@ -0,0 +1,170 @@
#!/usr/bin/env php
<?php
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
// List all files
$files = scandir(__DIR__.'/embed');
$files = array_filter(array_map(function ($f) {
if (!is_file(__DIR__.'/embed/'.$f)) return null;
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh$/', $f)) return null;
echo "Found: $f\n";
return $f;
}, $files));
sort($files);
$formatted = array_filter(array_map(function ($f) {
return "\"files/embed/$f\"";
}, $files));
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
"#begin staticfiles\n".
"set(COMPONENT_EMBED_FILES\n ".
implode("\n ", $formatted) . ")\n".
"#end staticfiles",
$cmake);
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
// Generate a list of files
$num = 0;
$enum_keys = array_map(function ($f) use(&$num) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
return 'FILE_'. $a.' = '.($num++);
}, $files);
$keylist = implode(",\n ", $enum_keys);
$struct_array = [];
$externs = array_map(function ($f) use (&$struct_array) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
$start = '_binary_'. strtolower($a).'_start';
$end = '_binary_'. strtolower($a).'_end';
static $mimes = array(
'txt' => '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', <<<FILE
// Do not change, auto-generated by gen_staticfiles.php
#ifndef _EMBEDDED_FILES_ENUM_H
#define _EMBEDDED_FILES_ENUM_H
#include <stddef.h>
#include <stdint.h>
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", <<<FILE
// Do not change, auto-generated by gen_staticfiles.php
#include <stdint.h>
#include "files_enum.h"
const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
$structlist
};
FILE
);

@ -0,0 +1,22 @@
#include <esp_err.h>
#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;
}

@ -0,0 +1,619 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <esp_log.h>
#include <esp_http_server.h>
#include <rom/queue.h>
#include <lwip/ip4_addr.h>
#include <sys/param.h>
#include <common_utils/utils.h>
#include <fileserver/token_subs.h>
#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, "&#34;", 5));
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "&#39;", 5));
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "&lt;", 4));
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "&gt;", 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;
}

@ -0,0 +1,9 @@
set(COMPONENT_ADD_INCLUDEDIRS
"include")
set(COMPONENT_SRCDIRS
"src")
set(COMPONENT_REQUIRES tcpip_adapter esp_http_server common_utils)
register_component()

@ -0,0 +1,4 @@
Functions enriching the HTTP server bundled with ESP-IDF.
This package includes HTTP-related utilities, captive
portal implementation, and a cookie-based session system with a
key-value store and expirations.

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,32 @@
#ifndef HTTPD_UTILS_CAPTIVE_H
#define HTTPD_UTILS_CAPTIVE_H
#include <esp_http_server.h>
#include <esp_err.h>
/**
* Redirect if needed when a captive portal capture is detected
*
* @param r
* @return ESP_OK on redirect, ESP_ERR_NOT_FOUND if not needed, other error on failure
*/
esp_err_t httpd_captive_redirect(httpd_req_t *r);
/**
* Get URL to redirect to. WEAK.
*
* @param r
* @param buf
* @param maxlen
* @return http(s)://foo.bar/
*/
esp_err_t httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen);
/**
* Get captive portal domain. WEAK.
*
* @return foo.bar
*/
const char * httpd_captive_redirect_get_domain();
#endif //HTTPD_UTILS_CAPTIVE_H

@ -0,0 +1,13 @@
#ifndef HTTPD_FDIPV4_H
#define HTTPD_FDIPV4_H
/**
* Get IP address for a FD
*
* @param fd
* @param[out] ipv4
* @return success
*/
esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4);
#endif //HTTPD_FDIPV4_H

@ -0,0 +1,16 @@
#ifndef HTTPD_UTILS_REDIRECT_H
#define HTTPD_UTILS_REDIRECT_H
#include <esp_http_server.h>
#include <esp_err.h>
/**
* Redirect to other URI - sends a HTTP response from the http server
*
* @param r - request
* @param uri - target uri
* @return success
*/
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri);
#endif // HTTPD_UTILS_REDIRECT_H

@ -0,0 +1,55 @@
/**
* Session store system main include file
*
* Created on 2019/07/13.
*/
#ifndef HTTPD_UTILS_SESSION_H
#define HTTPD_UTILS_SESSION_H
#include "session_kvmap.h"
#include "session_store.h"
// Customary keys
/** Session key for OK flash message */
#define SESS_FLASH_OK "flash_ok"
/** Session key for error flash message */
#define SESS_FLASH_ERR "flash_err"
/** Session key for a "logged in" flag. Value is 1 if logged in. */
#define SESS_AUTHED "authed"
// ..
/**
* Redirect to /login form if unauthed,
* but also retrieve the session key-value store for further use
*/
#define HTTP_GET_AUTHED_SESSION(kvstore, r) do { \
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA); \
if (NULL == kvstore || NULL == sess_kv_map_get(kvstore, SESS_AUTHED)) { \
return httpd_redirect_to((r), "/login"); \
} \
} while(0)
/**
* Start or resume a session without checking for authed status.
* When started, the session cookie is added to the response immediately.
*
* @param[out] kvstore - a place to store the allocated or retrieved session kvmap
* @param[in] r - request
* @return success
*/
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **kvstore, httpd_req_t *r);
/**
* Redirect to the login form if unauthed.
* This is the same as `HTTP_GET_AUTHED_SESSION`, except the kvstore variable is
* not needed in the uri handler calling this, so it is declared internally.
*/
#define HTTP_REDIRECT_IF_UNAUTHED(r) do { \
sess_kv_map_t *_kvstore; \
HTTP_GET_AUTHED_SESSION(_kvstore, r); \
} while(0)
#endif // HTTPD_UTILS_SESSION_H

@ -0,0 +1,77 @@
/**
* Simple key-value map for session data storage.
* Takes care of dynamic allocation and cleanup.
*
* Created on 2019/01/28.
*/
#ifndef SESSION_KVMAP_H
#define SESSION_KVMAP_H
/**
* Prototype for a free() func to clean up session-held objects
*/
typedef void (*sess_kv_free_func_t)(void *obj);
typedef struct sess_kv_map sess_kv_map_t;
#define SESS_KVMAP_KEY_LEN 16
/**
* Allocate a new session key-value store
*
* @return the store, NULL on error
*/
sess_kv_map_t *sess_kv_map_alloc(void);
/**
* Free the session kv store.
*
* @param head - store head
*/
void sess_kv_map_free(void *head);
/**
* Get a value from the session kv store.
*
* @param head - store head
* @param key - key to get a value for
* @return the value, or NULL if not found
*/
void *sess_kv_map_get(sess_kv_map_t *head, const char *key);
/**
* Get and remove a value from the session store.
*
* The free function is not called in this case and the recipient is
* responsible for cleaning it up correctly.
*
* @param head - store head
* @param key - key to get a value for
* @return the value, or NULL if not found
*/
void * sess_kv_map_take(sess_kv_map_t *head, const char *key);
/**
* Remove an entry from the session by its key name.
* The slot is not free'd yet, but is made available for reuse.
*
* @param head - store head
* @param key - key to remove
* @return success
*/
esp_err_t sess_kv_map_remove(sess_kv_map_t *head, const char *key);
/**
* Set a key value. If there is an old value stored, it will be freed by its free function and replaced by the new one.
* Otherwise a new slot is allocated for it, or a previously released one is reused.
*
* @param head - store head
* @param key - key to assign to
* @param value - new value
* @param free_fn - value free func
* @return success
*/
esp_err_t sess_kv_map_set(sess_kv_map_t *head, const char *key, void *value, sess_kv_free_func_t free_fn);
#endif //SESSION_KVMAP_H

@ -0,0 +1,100 @@
/**
* Cookie-based session store
*/
#ifndef SESSION_STORE_H
#define SESSION_STORE_H
#include "esp_http_server.h"
#define SESSION_EXPIRY_TIME_S 60*30
#define SESSION_COOKIE_NAME "SESSID"
/** function that frees a session data object */
typedef void (*sess_data_free_fn_t)(void *);
enum session_find_action {
SESS_DROP, SESS_GET_DATA
};
/**
* Find session and either get data, or drop it.
*
* @param cookie
* @param action
* @return
*/
void *session_find_and(const char *cookie, enum session_find_action action);
/**
* Initialize the session store.
* Safely empty it if initialized
*/
void session_store_init(void);
// placeholder for when no data is stored
#define SESSION_DUMMY ((void *) 1)
/**
* Create a new session. Data must not be NULL, because it wouldn't be possible
* to distinguish between NULL value and session not found in return values.
* It can be e.g. 1 if no data storage is needed.
*
* @param data - data object to attach to the session
* @param free_fn - function that disposes of the data when the session expires
* @return NULL on error, or the new session ID. This is a live pointer into the session structure,
* must be copied if stored, as it can become invalid at any time
*/
const char *session_new(void *data, sess_data_free_fn_t free_fn);
/**
* Find a session by it's ID (from a cookie)
*
* @param cookie - session ID string
* @return session data (void*), or NULL
*/
void *session_find(const char *cookie);
/**
* Loop through all sessions and drop these that expired.
*/
void session_drop_expired(void);
/**
* Drop a session by its ID. Does nothing if not found.
*
* @param cookie - session ID string
*/
void session_drop(const char *cookie);
/**
* Parse the Cookie header from a request, and do something with the corresponding session.
*
* To also delete the cookie, use req_delete_session_cookie(r)
*
* @param r - request
* @param action - what to do with the session
* @return session data, NULL if removed or not found
*/
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action);
/**
* Add a header that deletes the session cookie
*
* @param r - request
*/
void httpd_resp_delete_session_cookie(httpd_req_t *r);
/**
* Add a header that sets the session cookie.
*
* This must be called after creating a session (e.g. user logged in) to make it persistent.
*
* @attention NOT RE-ENTRANT, CAN'T BE USED AGAIN UNTIL THE REQUEST IT WAS CALLED FOR IS DISPATCHED.
*
* @param r - request
* @param cookie - cookie ID
*/
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie);
#endif //SESSION_STORE_H

@ -0,0 +1,106 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/param.h>
#include "httpd_utils/captive.h"
#include "httpd_utils/fd_to_ipv4.h"
#include "httpd_utils/redirect.h"
#include <common_utils/utils.h>
static const char *TAG="captive";
const char * __attribute__((weak))
httpd_captive_redirect_get_domain(void)
{
return "fb_node.captive";
}
esp_err_t __attribute__((weak))
httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen)
{
buf = append(buf, "http://", &maxlen);
buf = append(buf, httpd_captive_redirect_get_domain(), &maxlen);
append(buf, "/", &maxlen);
return ESP_OK;
}
esp_err_t httpd_captive_redirect(httpd_req_t *r)
{
// must be static to survive being used in the redirect header
static char s_buf[64];
wifi_mode_t mode = 0;
esp_wifi_get_mode(&mode);
// Check if we have an softap interface. No point checking IPs and hostnames if the client can't be on AP.
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_NULL) {
goto no_redirect;
}
int fd = httpd_req_to_sockfd(r);
tcpip_adapter_ip_info_t apip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &apip);
u32_t client_addr;
if(ESP_OK != fd_to_ipv4(fd, &client_addr)) {
return ESP_FAIL;
}
ESP_LOGD(TAG, "[captive] Client addr = 0x%08x, ap addr 0x%08x, ap nmask 0x%08x",
client_addr,
apip.ip.addr,
apip.netmask.addr
);
// Check if client IP looks like from our AP dhcps
if ((client_addr & apip.netmask.addr) != (apip.ip.addr & apip.netmask.addr)) {
ESP_LOGD(TAG, "[captive] Client not in AP IP range");
goto no_redirect;
}
// Get requested hostname from the header
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Host", s_buf, 64);
if (rv != ESP_OK) {
ESP_LOGW(TAG, "[captive] No host in request?");
goto no_redirect;
}
ESP_LOGD(TAG, "[captive] Candidate for redirect: %s%s", s_buf, r->uri);
// Never redirect if host is an IP
if (strlen(s_buf)>7) {
bool isIP = 1;
for (int x = 0; x < strlen(s_buf); x++) {
if (s_buf[x] != '.' && (s_buf[x] < '0' || s_buf[x] > '9')) {
isIP = 0;
break;
}
}
if (isIP) {
ESP_LOGD(TAG, "[captive] Access via IP, no redirect needed");
goto no_redirect;
}
}
// Redirect if host differs
// - this can be e.g. connectivitycheck.gstatic.com or the equivalent for ios
if (0 != strcmp(s_buf, httpd_captive_redirect_get_domain())) {
ESP_LOGD(TAG, "[captive] Host differs, redirecting...");
httpd_captive_redirect_get_url(r, s_buf, 64);
return httpd_redirect_to(r, s_buf);
} else {
ESP_LOGD(TAG, "[captive] Host is OK");
goto no_redirect;
}
no_redirect:
return ESP_ERR_NOT_FOUND;
}

@ -0,0 +1,42 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include "httpd_utils/fd_to_ipv4.h"
static const char *TAG = "fd2ipv4";
/**
* Get IP address for a FD
*
* @param fd
* @param[out] ipv4
* @return success
*/
esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4)
{
struct sockaddr_in6 addr;
size_t len = sizeof(addr);
int rv = getpeername(fd, (struct sockaddr *) &addr, &len);
if (rv != 0) {
ESP_LOGE(TAG, "Failed to get IP addr for fd %d", fd);
return ESP_FAIL;
}
uint32_t client_addr = 0;
if (addr.sin6_family == AF_INET6) {
// this would fail in a real ipv6 network
// with ipv4 the addr is simply in the last ipv6 byte
struct sockaddr_in6 *s = &addr;
client_addr = s->sin6_addr.un.u32_addr[3];
}
else {
struct sockaddr_in *s = (struct sockaddr_in *) &addr;
client_addr = s->sin_addr.s_addr;
}
*ipv4 = client_addr;
return ESP_OK;
}

@ -0,0 +1,20 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include "httpd_utils/redirect.h"
static const char *TAG="redirect";
esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri)
{
ESP_LOGD(TAG, "Redirect to %s", uri);
httpd_resp_set_hdr(r, "Location", uri);
httpd_resp_set_status(r, "303 See Other");
httpd_resp_set_type(r, HTTPD_TYPE_TEXT);
const char *msg = "Redirect";
return httpd_resp_send(r, msg, -1);
}

@ -0,0 +1,181 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <rom/queue.h>
#include <malloc.h>
#include <assert.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#include "httpd_utils/session_kvmap.h"
static const char *TAG = "sess_kvmap";
// this struct is opaque, a stub like this is sufficient for the head pointer.
struct sess_kv_entry;
/** Session head structure, dynamically allocated */
SLIST_HEAD(sess_kv_map, sess_kv_entry);
struct sess_kv_entry {
SLIST_ENTRY(sess_kv_entry) link;
char key[SESS_KVMAP_KEY_LEN];
void *value;
sess_kv_free_func_t free_fn;
};
struct sess_kv_map *sess_kv_map_alloc(void)
{
ESP_LOGD(TAG, "kv store alloc");
struct sess_kv_map *map = malloc(sizeof(struct sess_kv_map));
assert(map);
SLIST_INIT(map);
return map;
}
void sess_kv_map_free(void *head_v)
{
struct sess_kv_map* head = head_v;
ESP_LOGD(TAG, "kv store free");
assert(head);
struct sess_kv_entry *item, *tmp;
SLIST_FOREACH_SAFE(item, head, link, tmp) {
if (item->free_fn) {
item->free_fn(item->value);
free(item);
}
}
free(head);
}
void * sess_kv_map_get(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store get %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
ESP_LOGD(TAG, "got ok");
return item->value;
}
}
ESP_LOGD(TAG, "not found in store");
return NULL;
}
void * sess_kv_map_take(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store take %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
item->key[0] = 0;
item->free_fn = NULL;
ESP_LOGD(TAG, "taken ok");
return item->value;
}
}
ESP_LOGD(TAG, "not found in store");
return NULL;
}
esp_err_t sess_kv_map_remove(struct sess_kv_map *head, const char *key)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv store remove %s", key);
struct sess_kv_entry *item;
SLIST_FOREACH(item, head, link) {
if (0==strcmp(item->key, key)) {
if (item->free_fn) {
item->free_fn(item->value);
}
item->key[0] = 0;
item->value = NULL;
item->free_fn = NULL;
return ESP_OK;
}
}
ESP_LOGD(TAG, "couldn't remove, not found: %s", key);
return ESP_ERR_NOT_FOUND;
}
esp_err_t sess_kv_map_set(struct sess_kv_map *head, const char *key, void *value, sess_kv_free_func_t free_fn)
{
assert(head);
assert(key);
ESP_LOGD(TAG, "kv set value for key %s", key);
size_t key_len = strlen(key);
if (key_len > SESS_KVMAP_KEY_LEN-1) {
ESP_LOGE(TAG, "Key too long: %s", key);
// discard illegal key
return ESP_FAIL;
}
if (key_len == 0) {
ESP_LOGE(TAG, "Key too short: \"%s\"", key);
// discard illegal key
return ESP_FAIL;
}
struct sess_kv_entry *item = NULL;
struct sess_kv_entry *empty_item = NULL; // found item with no content
SLIST_FOREACH(item, head, link) {
ESP_LOGD(TAG, "test item with key %s, ptr %p > %p", item->key, item, item->link.sle_next);
if (0 == item->key[0]) {
ESP_LOGD(TAG, "found an empty slot");
empty_item = item;
}
else if (0==strcmp(item->key, key)) {
ESP_LOGD(TAG, "old value replaced");
if (item->free_fn) {
item->free_fn(item->value);
}
item->value = value;
item->free_fn = free_fn;
return ESP_OK;
} else {
ESP_LOGD(TAG, "skip this one");
}
}
struct sess_kv_entry *new_item = NULL;
// insert new or reuse an empty item
if (empty_item) {
new_item = empty_item;
ESP_LOGD(TAG, "empty item reused (%p)", new_item);
} else {
ESP_LOGD(TAG, "alloc new item");
// key not found, add a new entry.
new_item = malloc(sizeof(struct sess_kv_entry));
if (!new_item) {
ESP_LOGE(TAG, "New entry alloc failed");
return ESP_ERR_NO_MEM;
}
}
strcpy(new_item->key, key);
new_item->free_fn = free_fn;
new_item->value = value;
if (!empty_item) {
ESP_LOGD(TAG, "insert new item into list");
// this item was malloc'd
SLIST_INSERT_HEAD(head, new_item, link);
}
return ESP_OK;
}

@ -0,0 +1,220 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <malloc.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <esp_http_server.h>
#include <rom/queue.h>
#include "httpd_utils/session_store.h"
// TODO add a limit on simultaneously open sessions (can cause memory exhaustion DoS)
#define COOKIE_LEN 32
static const char *TAG = "session";
struct session {
char cookie[COOKIE_LEN + 1];
void *data;
TickType_t last_activity_time;
LIST_ENTRY(session) link;
sess_data_free_fn_t free_fn;
};
static LIST_HEAD(sessions_, session) s_store;
static SemaphoreHandle_t sess_store_lock = NULL;
static bool sess_store_inited = false;
void session_store_init(void)
{
if (sess_store_inited) {
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
struct session *it, *tit;
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
ESP_LOGW(TAG, "Session cookie expired: \"%s\"", it->cookie);
if (it->free_fn) it->free_fn(it->data);
// no relink, we dont care if the list breaks after this - we're removing all of it
free(it);
}
}
LIST_INIT(&s_store);
xSemaphoreGive(sess_store_lock);
} else {
LIST_INIT(&s_store);
sess_store_lock = xSemaphoreCreateMutex();
sess_store_inited = true;
}
}
/**
* Fill buffer with base64 symbols. Does not add a trailing null byte
*
* @param buf
* @param len
*/
static void esp_fill_random_alnum(char *buf, size_t len)
{
#define alphabet_len 64
static const char alphabet[alphabet_len] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
unsigned int seed = xTaskGetTickCount();
assert(buf != NULL);
for (int i = 0; i < len; i++) {
int index = rand_r(&seed) % alphabet_len;
*buf++ = (uint8_t) alphabet[index];
}
}
const char *session_new(void *data, sess_data_free_fn_t free_fn)
{
assert(data != NULL);
struct session *item = malloc(sizeof(struct session));
if (item == NULL) return NULL;
item->data = data;
item->free_fn = free_fn;
esp_fill_random_alnum(item->cookie, COOKIE_LEN);
item->cookie[COOKIE_LEN] = 0; // add the terminator
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
item->last_activity_time = xTaskGetTickCount();
LIST_INSERT_HEAD(&s_store, item, link);
}
xSemaphoreGive(sess_store_lock);
ESP_LOGD(TAG, "New HTTP session: %s", item->cookie);
return item->cookie;
}
void *session_find_and(const char *cookie, enum session_find_action action)
{
// no point in searching if the length is wrong
if (strlen(cookie) != COOKIE_LEN) {
ESP_LOGW(TAG, "Wrong session cookie length: \"%s\"", cookie);
return NULL;
}
struct session *it = NULL;
bool found = false;
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
LIST_FOREACH(it, &s_store, link) {
if (0==strcmp(it->cookie, cookie)) {
ESP_LOGD(TAG, "Session cookie matched: \"%s\"", cookie);
it->last_activity_time = xTaskGetTickCount();
found = true;
break;
}
}
if (found && action == SESS_DROP) {
if (it->free_fn) it->free_fn(it->data);
LIST_REMOVE(it, link);
free(it);
ESP_LOGD(TAG, "Dropped session: \"%s\"", cookie);
}
}
xSemaphoreGive(sess_store_lock);
if (found) {
if (action == SESS_DROP) {
// it was dropped inside the guarded block
// the return value is not used with DROP
return NULL;
}
else if(action == SESS_GET_DATA) {
return it->data;
}
}
ESP_LOGW(TAG, "Session cookie not found: \"%s\"", cookie);
return NULL;
}
void *session_find(const char *cookie)
{
return session_find_and(cookie, SESS_GET_DATA);
}
void session_drop(const char *cookie)
{
session_find_and(cookie, SESS_DROP);
}
void session_drop_expired(void)
{
struct session *it;
struct session *tit;
xSemaphoreTake(sess_store_lock, portMAX_DELAY);
{
TickType_t now = xTaskGetTickCount();
LIST_FOREACH_SAFE(it, &s_store, link, tit) {
TickType_t elapsed = now - it->last_activity_time;
if (elapsed > pdMS_TO_TICKS(SESSION_EXPIRY_TIME_S*1000)) {
ESP_LOGD(TAG, "Session cookie expired: \"%s\"", it->cookie);
if (it->free_fn) it->free_fn(it->data);
LIST_REMOVE(it, link);
free(it);
}
}
}
xSemaphoreGive(sess_store_lock);
}
void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action)
{
// this could be called periodically, but it's sufficient to run it at each request
// it won't slow anything down unless there are hundreds of sessions
session_drop_expired();
static char buf[256];
esp_err_t rv = httpd_req_get_hdr_value_str(r, "Cookie", buf, 256);
if (rv == ESP_OK || rv == ESP_ERR_HTTPD_RESULT_TRUNC) {
ESP_LOGD(TAG, "Cookie header: %s", buf);
// probably OK, see if we have a cookie
char *start = strstr(buf, SESSION_COOKIE_NAME"=");
if (start != 0) {
start += strlen(SESSION_COOKIE_NAME"=");
char *end = strchr(start, ';');
if (end != NULL) *end = 0;
ESP_LOGD(TAG, "Cookie is: %s", start);
return session_find_and(start, action);
}
} else {
ESP_LOGD(TAG, "No cookie.");
}
return NULL;
}
void httpd_resp_delete_session_cookie(httpd_req_t *r)
{
httpd_resp_set_hdr(r, "Set-Cookie", SESSION_COOKIE_NAME"=");
}
// Static because the value is passed and stored by reference, so it wouldn't live long enough if it was on stack,
// and there also isn't any hook for freeing it if we used malloc(). This is an SDK bug.
static char cookie_hdr_buf[COOKIE_LEN + 10];
// !!! this must not be called concurrently from a different thread.
// That is no problem so long as the server stays single-threaded
void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie)
{
snprintf(cookie_hdr_buf, COOKIE_LEN + 10, "SESSID=%s", cookie);
httpd_resp_set_hdr(r, "Set-Cookie", cookie_hdr_buf);
}

@ -0,0 +1,41 @@
/**
* TODO file description
*
* Created on 2019/07/13.
*/
#ifndef SESSION_UTILS_C_H
#define SESSION_UTILS_C_H
#include <esp_err.h>
#include <esp_http_server.h>
#include "httpd_utils/session_kvmap.h"
#include "httpd_utils/session_store.h"
/**
* Start or resume a session.
*/
esp_err_t HTTP_GET_SESSION(sess_kv_map_t **ppkvstore, httpd_req_t *r)
{
sess_kv_map_t *kvstore;
kvstore = httpd_req_find_session_and((r), SESS_GET_DATA);
if (NULL == kvstore) {
kvstore = sess_kv_map_alloc();
if (!kvstore) return ESP_ERR_NO_MEM;
const char *cookie = session_new(kvstore, sess_kv_map_free);
if (cookie == NULL) {
// session alloc failed
sess_kv_map_free(kvstore);
*ppkvstore = NULL;
return ESP_ERR_NO_MEM;
}
httpd_resp_set_session_cookie(r, cookie);
}
*ppkvstore = kvstore;
return ESP_OK;
}
#endif //SESSION_UTILS_C_H

@ -16,7 +16,17 @@ set(COMPONENT_SRCS
"graphics/font.c" "graphics/font.c"
"graphics/drawing.c" "graphics/drawing.c"
"graphics/bitmaps.c" "graphics/bitmaps.c"
"web/websrv.c"
"files/files_enum.c"
"utils.c"
) )
set(COMPONENT_ADD_INCLUDEDIRS "." "liquid" "graphics") 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() register_component()

@ -16,6 +16,13 @@ static float measurement_celsius;
static const adc_atten_t atten = ADC_ATTEN_DB_0; static const adc_atten_t atten = ADC_ATTEN_DB_0;
static const adc_unit_t unit = ADC_UNIT_1; 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 void analog_service(void *arg);
static TaskHandle_t hAnalog; static TaskHandle_t hAnalog;
@ -63,7 +70,15 @@ static void __attribute__((noreturn)) analog_service(void *arg) {
measurement_celsius = celsius; 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));
} }
} }

@ -7,6 +7,11 @@
#ifndef REFLOWER_ANALOG_H #ifndef REFLOWER_ANALOG_H
#define 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(); void analog_init();
float analog_read(); float analog_read();

@ -7,6 +7,8 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
#include <stdio.h> #include <stdio.h>
#include <web/websrv.h>
#include <esp_wifi.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_system.h" #include "esp_system.h"
@ -15,11 +17,68 @@
#include "knob.h" #include "knob.h"
#include "gui.h" #include "gui.h"
#include "analog.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() 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 = { gpio_config_t output = {
.pin_bit_mask = (1<<22), .pin_bit_mask = (1<<22),
@ -27,11 +86,29 @@ void __attribute__((noreturn)) app_main()
}; };
gpio_config(&output); 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(); gui_init();
knob_init(); knob_init();
analog_init(); analog_init();
try_reconn_if_have_wifi_creds();
websrv_init();
bool level = 0; bool level = 0;
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));

@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 440">
<style>
.ticks, .frame {
stroke-width: 1px;
fill: none;
stroke: black;
}
path.major {
stroke-width: 2px;
}
.ylabels text {
font-size: 10px;
text-anchor: end;
font-family: Droid Sans, sans-serif;
vertical-align: middle;
}
.grid {
stroke-dasharray: 4;
stroke: #999;
}
.series {
stroke-width: 2px;
fill: none;
}
</style>
<g transform="translate(50,15)">
<path d="M100,0 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400"
class="grid" transform="translate(0,0)" id="grid-v" />
<path d="M0,100 h600m-600,100 h600m-600,100 h600m-600,100"
class="grid" stroke-dashoffset="0" id="grid-h" />
<path d="M-10,0 h10m-10,100 h10m-10,100 h10m-10,100 h10m-10,100 h10" class="ticks" />
<path d="M-5,10
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5" class="ticks" />
<g class="series">
<path d="M0,400L600,0" stroke="blue" id="ser-set" />
<path d="M0,0L300,100L600,400" stroke="red" id="ser-actual" />
</g>
<path d="M0,0h600v400h-600Z" class="frame" />
<g class="ylabels" transform="translate(0,3)">
<text x="-15" y="0">400 °C</text>
<text x="-15" y="100">300 °C</text>
<text x="-15" y="200">200 °C</text>
<text x="-15" y="300">100 °C</text>
<text x="-15" y="400">0 °C</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Breadflow Web Control</title>
<style>
* {
box-sizing: content-box;
}
h1 {
text-align: center;
}
#ctab {
width: 1400px;
margin: 0 auto;
border: 1px solid #ccc;
border-collapse: collapse;
}
#td-side {
border-left: 1px solid #ccc;
width: 340px;
padding: 15px;
vertical-align: top;
padding-top: 22px;
}
#td-img {
vertical-align: top;
}
</style>
</head>
<body>
<h1>Breadflow {version}</h1>
<table id="ctab">
<tr>
<td id="td-img">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 430">
<style>
.ticks, .frame {
stroke-width: 1px;
fill: none;
stroke: black;
}
path.major {
stroke-width: 2px;
}
.ylabels text {
font-size: 10px;
text-anchor: end;
font-family: Droid Sans, sans-serif;
vertical-align: middle;
}
.grid {
stroke-dasharray: 2;
stroke: #dbdbdb;
}
.series {
stroke-width: 2px;
fill: none;
}
</style>
<g transform="translate(50,15)">
<path d="M100,0 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400m100,-400 v400"
class="grid" transform="translate(0,0)" id="grid-v" />
<path d="M0,100 h600m-600,100 h600m-600,100 h600m-600,100"
class="grid" stroke-dashoffset="0" id="grid-h" />
<path d="M-10,0 h10m-10,100 h10m-10,100 h10m-10,100 h10m-10,100 h10" class="ticks" />
<path d="M-5,10
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,20
h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5m-5,10h5" class="ticks" />
<g class="series">
<path d="{ser-set}" stroke="blue" id="ser-set" /><!--M0,400L600,0-->
<path d="{ser-act}" stroke="red" id="ser-act" /><!--M0,0L300,100L600,400-->
</g>
<path d="M0,0h600v400h-600Z" class="frame" />
<g class="ylabels" transform="translate(0,3)">
<text x="-15" y="0">400 °C</text>
<text x="-15" y="100">300 °C</text>
<text x="-15" y="200">200 °C</text>
<text x="-15" y="300">100 °C</text>
<text x="-15" y="400">0 °C</text>
</g>
</g>
</svg>
</td>
<td id="td-side">
Sidebar
</td>
</tr>
</table>
<script>
var Qi = function (x) { return document.getElementById(x) };
function update(data) {
if (data) {
let rows = data.split('\x1e');
rows.forEach(function (v) {
let [k, va] = v.split('\x1f');
switch (k) {
case 'ser-set':
Qi('ser-set').setAttribute('d', va);
break;
case 'ser-act':
Qi('ser-act').setAttribute('d', va);
break;
case 'timeshift':
Qi('grid-v').setAttribute('transform', 'translate(-'+(va*5)+',0)');
Qi('grid-h').setAttribute('stroke-dashoffset', -va * 5);
break;
}
});
} else {
var xhr=new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState===4){
if (xhr.status===200) {
update(xhr.responseText);
}
setTimeout(update, 500);
}
};
xhr.onerror = function () {
setTimeout(update, 500);
};
xhr.open('GET', '/data');
xhr.send();
}
}
setTimeout(update, 500);
</script>

@ -0,0 +1,15 @@
// Generated by 'rebuild_file_tables'
#include <stdint.h>
#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;

@ -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

@ -0,0 +1,163 @@
#!/usr/bin/env php
<?php
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
// List all files
$files = scandir(__DIR__.'/embed');
$files = array_filter(array_map(function ($f) {
if (!is_file(__DIR__.'/embed/'.$f)) return null;
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh|\.ignore\..*$/', $f)) return null;
echo "Found: $f\n";
return $f;
}, $files));
sort($files);
$formatted = array_filter(array_map(function ($f) {
return "\"files/embed/$f\"";
}, $files));
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
"#begin staticfiles\n".
"# generated by rebuild_file_tables\n".
"set(COMPONENT_EMBED_FILES\n ".
implode("\n ", $formatted) . ")\n".
"#end staticfiles",
$cmake);
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
// Generate a list of files
$num = 0;
$enum_keys = array_map(function ($f) use(&$num) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
return 'FILE_'. $a.' = '.($num++);
}, $files);
$keylist = implode(",\n ", $enum_keys);
$struct_array = [];
$externs = array_map(function ($f) use (&$struct_array) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
$start = '_binary_'. strtolower($a).'_start';
$end = '_binary_'. strtolower($a).'_end';
static $mimes = array(
'txt' => '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', <<<FILE
// Generated by 'rebuild_file_tables'
#ifndef _EMBEDDED_FILES_ENUM_H
#define _EMBEDDED_FILES_ENUM_H
#include "fileserver/embedded_files.h"
enum embedded_file_id {
$keylist
};
#endif // _EMBEDDED_FILES_ENUM_H
FILE
);
$files_count = count($struct_array);
file_put_contents("files_enum.c", <<<FILE
// Generated by 'rebuild_file_tables'
#include <stdint.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
);

@ -5,7 +5,7 @@
#include "liquid.h" #include "liquid.h"
#include "graphics/drawing.h" #include "graphics/drawing.h"
#define BOOTANIM_TIME_MS 100 #define BOOTANIM_TIME_MS 2000
struct BootScene { struct BootScene {
struct Scene base; struct Scene base;

@ -59,6 +59,11 @@ static void deinit(struct MenuScene *self)
{ {
free(self->items); free(self->items);
self->items = NULL; self->items = NULL;
if (self->extra_deinit) {
self->extra_deinit((struct Scene *) self);
self->extra = NULL;
}
} }
struct MenuScene *NewScene_Menu(struct MenuItem *items, size_t items_len) { struct MenuScene *NewScene_Menu(struct MenuItem *items, size_t items_len) {
@ -68,6 +73,10 @@ struct MenuScene *NewScene_Menu(struct MenuItem *items, size_t items_len) {
scene->base.onInput = (Scene_onInput_t) onInput; scene->base.onInput = (Scene_onInput_t) onInput;
scene->base.deinit = (Scene_deinit_t) deinit; scene->base.deinit = (Scene_deinit_t) deinit;
// onChildReturn is free to set by subclass
// TODO long selected label scrolling on tick
scene->items = items; scene->items = items;
scene->items_len = items_len; scene->items_len = items_len;

@ -1,5 +1,5 @@
/** /**
* TODO file description * Scrollable menu
* *
* Created on 2020/01/05. * Created on 2020/01/05.
*/ */
@ -12,13 +12,26 @@
#define MENUITEM_LABEL_LEN 30 #define MENUITEM_LABEL_LEN 30
/**
* One item of the menu
*/
struct MenuItem { struct MenuItem {
// menu label
char label[MENUITEM_LABEL_LEN]; char label[MENUITEM_LABEL_LEN];
// item tag (useful with dynamically generated menus)
uint32_t tag; uint32_t tag;
}; };
struct MenuScene; struct MenuScene;
/**
* Selection handler.
*
* When an item is selected, this abstract method is fired to determine what should happen.
*
* The selected item is available as scene.selected.
* Return SceneEvent to take, e.g. to close the menu, or to open a sub-scene.
*/
typedef struct SceneEvent (*MenuScene_onSelect_t)(struct MenuScene *scene); typedef struct SceneEvent (*MenuScene_onSelect_t)(struct MenuScene *scene);
struct MenuScene { struct MenuScene {
@ -33,10 +46,21 @@ struct MenuScene {
int32_t selected; int32_t selected;
// selection handler // selection handler
MenuScene_onSelect_t onSelect; MenuScene_onSelect_t onSelect;
// Extra field for the instance's private use. // Extra field for the subclass's private use.
void *private; void *extra;
// fp for extra's deinit function
Scene_deinit_t extra_deinit;
}; };
/**
* Create a new items menu.
*
* This is normally called from a subclass.
*
* @param items - menu items on heap (the MenuScene will free them on close)
* @param items_len - item count
* @return the menu
*/
struct MenuScene *NewScene_Menu(struct MenuItem *items, size_t items_len); struct MenuScene *NewScene_Menu(struct MenuItem *items, size_t items_len);
#endif //REFLOWER_SCENE_MENU_H #endif //REFLOWER_SCENE_MENU_H

@ -1,8 +1,7 @@
#include "scenes.h"
#include "liquid.h"
#include <malloc.h> #include <malloc.h>
#include <stdio.h> #include <stdio.h>
#include "graphics/nokia.h" #include "scenes.h"
#include "liquid.h"
#include "graphics/drawing.h" #include "graphics/drawing.h"
#include "scene_number.h" #include "scene_number.h"

@ -9,20 +9,27 @@
#include <stdint.h> #include <stdint.h>
/**
* Options passed to the NumberScene constructor
*/
struct NumberSceneOpts { struct NumberSceneOpts {
// Screen title
char label[15]; char label[15];
// Unit added to the value
char unit[8]; char unit[8];
// Initial value
int32_t value; int32_t value;
// min value
int32_t min; int32_t min;
// max value
int32_t max; int32_t max;
}; };
/** /**
* Create number picker. * Create number picker.
* *
* The result is passed back as status.
*
* "opts" must be on heap and will be internally mutated. * "opts" must be on heap and will be internally mutated.
* The scene frees them on exit, returning the selected value as status.
* *
* @param opts * @param opts
* @return * @return

@ -12,11 +12,10 @@
struct Priv { struct Priv {
int32_t number; int32_t number;
Scene_deinit_t parent_deinit;
}; };
static struct SceneEvent onSelect(struct MenuScene *self) { static struct SceneEvent onSelect(struct MenuScene *self) {
struct Priv *priv = self->private; struct Priv *priv = self->extra;
if (self->selected == 0) { if (self->selected == 0) {
return SceneEvent_Close(0, NULL); return SceneEvent_Close(0, NULL);
@ -46,19 +45,15 @@ static struct SceneEvent onSelect(struct MenuScene *self) {
return SceneEvent_None(); return SceneEvent_None();
} }
static void deinit(struct MenuScene *self) static void priv_deinit(struct MenuScene *self)
{ {
struct Priv *priv = self->private; free(self->extra);
if (priv->parent_deinit) { self->extra = NULL;
priv->parent_deinit((struct Scene *) self);
}
free(self->private);
self->private = NULL;
} }
static struct SceneEvent onChildReturn(struct MenuScene *self, uint32_t tag, int32_t status, void *data) static struct SceneEvent onChildReturn(struct MenuScene *self, uint32_t tag, int32_t status, void *data)
{ {
struct Priv *priv = self->private; struct Priv *priv = self->extra;
if (tag == 99) { if (tag == 99) {
priv->number = status; priv->number = status;
snprintf(self->items[2].label, MENUITEM_LABEL_LEN, "Set=%d°C", priv->number); snprintf(self->items[2].label, MENUITEM_LABEL_LEN, "Set=%d°C", priv->number);
@ -88,10 +83,10 @@ struct Scene *NewScene_MenuTest() {
// private data added by the subclass // private data added by the subclass
struct Priv *priv = calloc(1, sizeof(struct Priv)); struct Priv *priv = calloc(1, sizeof(struct Priv));
priv->number = 0; priv->number = 0;
priv->parent_deinit = scene->base.deinit; scene->extra = priv;
scene->private = priv; scene->extra_deinit = (Scene_deinit_t) priv_deinit;
scene->base.deinit = (Scene_deinit_t) deinit; // add a child return handler (base does not use this)
scene->base.onChildReturn = (Scene_onChildReturn_t) onChildReturn; scene->base.onChildReturn = (Scene_onChildReturn_t) onChildReturn;
return (struct Scene *) scene; return (struct Scene *) scene;

@ -0,0 +1,70 @@
#include <soc/timer_group_struct.h>
#include <soc/timer_group_reg.h>
#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;
}

@ -0,0 +1,76 @@
/**
* Utilities and helpers
*/
#ifndef CSPEMU_UTILS_H
#define CSPEMU_UTILS_H
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include <nvs.h>
#include <common_utils/utils.h>
/** 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

@ -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

@ -0,0 +1,212 @@
#include <esp_log.h>
#include <esp_err.h>
#include <fileserver/token_subs.h>
#include <httpd_utils/captive.h>
#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, "<!DOCTYPE html><html><head>"
"<meta http-equiv=\"refresh\" content=\"10; url=/\">"
"</head>"
"<body>"
"Reboot requested. Reloading in 10 seconds.<br>"
"<a href=\"/\">Try now.</a>", -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;
}

@ -0,0 +1,14 @@
/**
* Integrated webserver
*
* Created on 2019/07/13.
*/
#ifndef CSPEMU_WEBSRV_H
#define CSPEMU_WEBSRV_H
#include <esp_err.h>
esp_err_t websrv_init(void);
#endif //CSPEMU_WEBSRV_H
Loading…
Cancel
Save