add webserver components, some webserver fices and improvements in templating, add real time history chart
parent
ed53611e0f
commit
ef7866a29f
@ -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, """, 5)); |
||||||
|
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "'", 5)); |
||||||
|
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "<", 4)); |
||||||
|
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, ">", 4)); |
||||||
|
} |
||||||
|
|
||||||
|
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
/* encode for JS. returns 0 or 1 - 1 = success */ |
||||||
|
static esp_err_t send_js_chunk(httpd_req_t *r, const char *data, ssize_t len) |
||||||
|
{ |
||||||
|
assert(r); |
||||||
|
assert(data); |
||||||
|
|
||||||
|
int start = 0, end = 0; |
||||||
|
char c; |
||||||
|
if (len < 0) len = (int) strlen(data); |
||||||
|
if (len==0) return ESP_OK; |
||||||
|
|
||||||
|
for (end = 0; end < len; end++) { |
||||||
|
c = data[end]; |
||||||
|
if (c == 0) { |
||||||
|
// we found EOS
|
||||||
|
break; // not return - the last chunk is printed after the loop
|
||||||
|
} |
||||||
|
|
||||||
|
if (c == '"' || c == '\\' || c == '/' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') { |
||||||
|
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); |
||||||
|
start = end + 1; |
||||||
|
} |
||||||
|
|
||||||
|
if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "\\\"", 2)); |
||||||
|
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "\\'", 2)); |
||||||
|
else if (c == '\\') ESP_TRY(httpd_resp_send_chunk(r, "\\\\", 2)); |
||||||
|
else if (c == '/') ESP_TRY(httpd_resp_send_chunk(r, "\\/", 2)); |
||||||
|
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "\\u003C", 6)); |
||||||
|
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "\\u003E", 6)); |
||||||
|
else if (c == '\n') ESP_TRY(httpd_resp_send_chunk(r, "\\n", 2)); |
||||||
|
else if (c == '\r') ESP_TRY(httpd_resp_send_chunk(r, "\\r", 2)); |
||||||
|
} |
||||||
|
|
||||||
|
if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape) |
||||||
|
{ |
||||||
|
switch (escape) { |
||||||
|
default: // this enum should be exhaustive, but in case something went wrong, just print it verbatim
|
||||||
|
|
||||||
|
case TPL_ESCAPE_NONE: |
||||||
|
return httpd_resp_send_chunk(r, buf, len); |
||||||
|
|
||||||
|
case TPL_ESCAPE_HTML: |
||||||
|
return send_html_chunk(r, buf, len); |
||||||
|
|
||||||
|
case TPL_ESCAPE_JS: |
||||||
|
return send_js_chunk(r, buf, len); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts) |
||||||
|
{ |
||||||
|
assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); |
||||||
|
const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; |
||||||
|
|
||||||
|
return httpd_send_static_file_struct(r, file, escape, opts); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts) |
||||||
|
{ |
||||||
|
if (0 == (opts & HTOPT_NO_HEADERS)) { |
||||||
|
ESP_TRY(httpd_resp_set_type(r, file->mime)); |
||||||
|
ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "max-age=86400, public, must-revalidate")); |
||||||
|
} |
||||||
|
|
||||||
|
ESP_TRY(httpd_resp_send_chunk_escaped(r, (const char *) file->start, (size_t)(file->end - file->start), escape)); |
||||||
|
|
||||||
|
if (0 == (opts & HTOPT_NO_CLOSE)) { |
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); |
||||||
|
} |
||||||
|
|
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t httpd_send_template_file(httpd_req_t *r, |
||||||
|
int file_index, |
||||||
|
template_subst_t replacer, |
||||||
|
void *context, |
||||||
|
uint32_t opts) |
||||||
|
{ |
||||||
|
assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); |
||||||
|
const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; |
||||||
|
return httpd_send_template_file_struct(r,file,replacer,context,opts); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t httpd_send_template_file_struct(httpd_req_t *r, |
||||||
|
const struct embedded_file_info *file, |
||||||
|
template_subst_t replacer, |
||||||
|
void *context, |
||||||
|
uint32_t opts) |
||||||
|
{ |
||||||
|
if (0 == (opts & HTOPT_NO_HEADERS)) { |
||||||
|
ESP_TRY(httpd_resp_set_type(r, file->mime)); |
||||||
|
ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "no-cache, no-store, must-revalidate")); |
||||||
|
} |
||||||
|
|
||||||
|
return httpd_send_template(r, (const char *) file->start, (size_t)(file->end - file->start), replacer, context, opts); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) |
||||||
|
{ |
||||||
|
assert(context); |
||||||
|
assert(token); |
||||||
|
|
||||||
|
struct tpl_kv_entry *entry; |
||||||
|
struct tpl_kv_list *head = context; |
||||||
|
SLIST_FOREACH(entry, head, link) { |
||||||
|
if (0==strcmp(entry->key, token)) { |
||||||
|
return httpd_resp_send_chunk_escaped(r, |
||||||
|
entry->subst_heap ? |
||||||
|
entry->subst_heap : |
||||||
|
entry->subst, |
||||||
|
-1, escape); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ESP_ERR_NOT_FOUND; |
||||||
|
} |
||||||
|
|
||||||
|
struct stacked_replacer_context { |
||||||
|
template_subst_t replacer0; |
||||||
|
void *context0; |
||||||
|
template_subst_t replacer1; |
||||||
|
void *context1; |
||||||
|
}; |
||||||
|
|
||||||
|
esp_err_t stacked_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) |
||||||
|
{ |
||||||
|
assert(context); |
||||||
|
assert(token); |
||||||
|
|
||||||
|
struct stacked_replacer_context *combo = context; |
||||||
|
|
||||||
|
if (ESP_OK == combo->replacer0(r, combo->context0, token, escape)) { |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
if (ESP_OK == combo->replacer1(r, combo->context1, token, escape)) { |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
return ESP_ERR_NOT_FOUND; |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t httpd_send_template(httpd_req_t *r, |
||||||
|
const char *template, ssize_t template_len, |
||||||
|
template_subst_t replacer, |
||||||
|
void *context, |
||||||
|
uint32_t opts) |
||||||
|
{ |
||||||
|
if (template_len < 0) template_len = strlen(template); |
||||||
|
|
||||||
|
// replacer and context may be NULL
|
||||||
|
assert(template); |
||||||
|
assert(r); |
||||||
|
|
||||||
|
// data end
|
||||||
|
const char * const end = template + template_len; |
||||||
|
|
||||||
|
// start of to-be-processed data
|
||||||
|
const char * pos = template; |
||||||
|
|
||||||
|
// start position for finding opening braces, updated after a failed match to avoid infinite loop on the same bad token
|
||||||
|
const char * searchpos = pos; |
||||||
|
|
||||||
|
// tokens must be copied to a buffer to allow adding the terminating null byte
|
||||||
|
char token_buf[MAX_TOKEN_LEN]; |
||||||
|
|
||||||
|
while (pos < end) { |
||||||
|
const char * openbr = strchr(searchpos, '{'); |
||||||
|
if (openbr == NULL) { |
||||||
|
// no more templates
|
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// this brace could start a valid template. check if it seems valid...
|
||||||
|
const char * closebr = strchr(openbr, '}'); |
||||||
|
if (closebr == NULL) { |
||||||
|
// there are no further closing braces, so it can't be a template
|
||||||
|
|
||||||
|
// we also know there can't be any more substitutions, because they would lack a closing } too
|
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// see if the braces content looks like a token
|
||||||
|
const char *t = openbr + 1; |
||||||
|
bool token_valid = true; |
||||||
|
struct tpl_kv_list substitutions_head = tpl_kv_init(); |
||||||
|
struct tpl_kv_entry *new_subst_pair = NULL; |
||||||
|
|
||||||
|
// a token can be either a name for replacement by the replacer func, or an include with static kv replacements
|
||||||
|
bool is_include = false; |
||||||
|
bool token_is_optional = false; |
||||||
|
const char *token_end = NULL; // points one char after the end of the token
|
||||||
|
|
||||||
|
// parsing the token
|
||||||
|
{ |
||||||
|
if (*t == '@') { |
||||||
|
ESP_LOGD(TAG, "Parsing an Include token"); |
||||||
|
is_include = true; |
||||||
|
t++; |
||||||
|
} |
||||||
|
|
||||||
|
enum { |
||||||
|
P_NAME, P_KEY, P_VALUE |
||||||
|
} state = P_NAME; |
||||||
|
|
||||||
|
const char *kv_start = NULL; |
||||||
|
while (t != closebr || state == P_VALUE) { |
||||||
|
char c = *t; |
||||||
|
|
||||||
|
if (state == P_NAME) { |
||||||
|
if (!((c >= 'a' && c <= 'z') || |
||||||
|
(c >= 'A' && c <= 'Z') || |
||||||
|
(c >= '0' && c <= '9') || |
||||||
|
c == '.' || c == '_' || c == '-' || c == ':')) { |
||||||
|
|
||||||
|
if (!is_include && c == '?') { |
||||||
|
token_end = t; |
||||||
|
token_is_optional = true; |
||||||
|
} else { |
||||||
|
if (is_include && c == '|') { |
||||||
|
token_end = t; |
||||||
|
state = P_KEY; |
||||||
|
kv_start = t + 1; |
||||||
|
// pipe separates the include's filename and literal substitutions
|
||||||
|
// we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
|
||||||
|
} |
||||||
|
else { |
||||||
|
token_valid = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else if (state == P_KEY) { |
||||||
|
if (!((c >= 'a' && c <= 'z') || |
||||||
|
(c >= 'A' && c <= 'Z') || |
||||||
|
(c >= '0' && c <= '9') || |
||||||
|
c == '.' || c == '_' || c == '-')) { |
||||||
|
if (c == '=') { |
||||||
|
new_subst_pair = calloc(1, sizeof(struct tpl_kv_entry)); |
||||||
|
const size_t klen = MIN(TPL_KV_KEY_LEN, t - kv_start); |
||||||
|
strncpy(new_subst_pair->key, kv_start, klen); |
||||||
|
new_subst_pair->key[klen] = 0; |
||||||
|
|
||||||
|
kv_start = t + 1; |
||||||
|
|
||||||
|
state = P_VALUE; |
||||||
|
// pipe separates the include's filename and literal substitutions
|
||||||
|
// we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else if (state == P_VALUE) { |
||||||
|
if (c == '|' || c == '}') { |
||||||
|
const size_t vlen = MIN(TPL_KV_SUBST_LEN, t - kv_start); |
||||||
|
strncpy(new_subst_pair->subst, kv_start, vlen); |
||||||
|
new_subst_pair->subst[vlen] = 0; |
||||||
|
|
||||||
|
// attach the kv pair to the list
|
||||||
|
SLIST_INSERT_HEAD(&substitutions_head, new_subst_pair, link); |
||||||
|
ESP_LOGD(TAG, "Adding subs kv %s -> %s", new_subst_pair->key, new_subst_pair->subst); |
||||||
|
new_subst_pair = NULL; |
||||||
|
|
||||||
|
kv_start = t + 1; // go past the pipe
|
||||||
|
state = P_KEY; |
||||||
|
|
||||||
|
if (t == closebr) { |
||||||
|
break; // found the ending brace, so let's quit the kv parse loop
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t++; |
||||||
|
} |
||||||
|
// clean up after a messed up subs kv pairs syntax
|
||||||
|
if (new_subst_pair != NULL) { |
||||||
|
free(new_subst_pair); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!token_valid) { |
||||||
|
// false match, include it in the block to send before the next token
|
||||||
|
searchpos = openbr + 1; |
||||||
|
ESP_LOGD(TAG, "Skip invalid token near %10s", openbr); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// now we know it looks like a substitution token
|
||||||
|
|
||||||
|
// flush data before the token
|
||||||
|
if (pos != openbr) ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (openbr - pos))); |
||||||
|
|
||||||
|
const char *token_start = openbr; |
||||||
|
|
||||||
|
tpl_escape_t escape = TPL_ESCAPE_NONE; |
||||||
|
|
||||||
|
// extract and terminate the token
|
||||||
|
size_t token_len = MIN(MAX_TOKEN_LEN-1, closebr - openbr - 1); |
||||||
|
if (token_end) { |
||||||
|
token_len = MIN(token_len, token_end - openbr - 1); |
||||||
|
} |
||||||
|
|
||||||
|
if (is_include) { |
||||||
|
token_start += 1; // skip the @
|
||||||
|
token_len -= 1; |
||||||
|
} else { |
||||||
|
if (0 == strncmp("h:", openbr + 1, 2)) { |
||||||
|
escape = TPL_ESCAPE_HTML; |
||||||
|
token_start += 2; |
||||||
|
token_len -= 2; |
||||||
|
} |
||||||
|
else if (0 == strncmp("j:", openbr + 1, 2)) { |
||||||
|
escape = TPL_ESCAPE_JS; |
||||||
|
token_start += 2; |
||||||
|
token_len -= 2; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
strncpy(token_buf, token_start+1, token_len); |
||||||
|
token_buf[token_len] = 0; |
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Token: %s", token_buf); |
||||||
|
|
||||||
|
esp_err_t rv; |
||||||
|
|
||||||
|
if (is_include) { |
||||||
|
ESP_LOGD(TAG, "Trying to include a sub-file"); |
||||||
|
|
||||||
|
const struct embedded_file_info *file = NULL; |
||||||
|
rv = www_get_static_file(token_buf, FILE_ACCESS_PROTECTED, &file); |
||||||
|
if (rv != ESP_OK) { |
||||||
|
ESP_LOGE(TAG, "Failed to statically include \"%s\" in a template - %s", token_buf, esp_err_to_name(rv)); |
||||||
|
// this will cause the token to be emitted verbatim
|
||||||
|
} else { |
||||||
|
ESP_LOGD(TAG, "Descending..."); |
||||||
|
|
||||||
|
// combine the two replacers
|
||||||
|
struct stacked_replacer_context combo = { |
||||||
|
.replacer0 = replacer, |
||||||
|
.context0 = context, |
||||||
|
.replacer1 = tpl_kv_replacer, |
||||||
|
.context1 = &substitutions_head |
||||||
|
}; |
||||||
|
|
||||||
|
rv = httpd_send_template_file_struct(r, file, stacked_replacer, &combo, HTOPT_INCLUDE); |
||||||
|
ESP_LOGD(TAG, "...back in parent"); |
||||||
|
} |
||||||
|
|
||||||
|
// tear down the list
|
||||||
|
tpl_kv_free(&substitutions_head); |
||||||
|
|
||||||
|
if (rv != ESP_OK) { |
||||||
|
// just send it verbatim...
|
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (replacer) { |
||||||
|
ESP_LOGD(TAG, "Running replacer for \"%s\" with escape %d", token_buf, escape); |
||||||
|
rv = replacer(r, context, token_buf, escape); |
||||||
|
|
||||||
|
if (rv != ESP_OK) { |
||||||
|
if (rv == ESP_ERR_NOT_FOUND) { |
||||||
|
ESP_LOGD(TAG, "Token rejected"); |
||||||
|
// optional token becomes empty string if not replaced
|
||||||
|
if (!token_is_optional) { |
||||||
|
ESP_LOGD(TAG, "Not optional, keeping verbatim"); |
||||||
|
// replacer rejected the token, keep it verbatim
|
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
ESP_LOGE(TAG, "Unexpected error from replacer func: 0x%02x - %s", rv, esp_err_to_name(rv)); |
||||||
|
return rv; |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
ESP_LOGD(TAG, "Not replacer"); |
||||||
|
// no replacer, only includes - used for 'static' files
|
||||||
|
if (!token_is_optional) { |
||||||
|
ESP_LOGD(TAG, "Token not optional, keeping verbatim"); |
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
searchpos = pos = closebr + 1; |
||||||
|
} |
||||||
|
|
||||||
|
if (0 == (opts & HTOPT_NO_CLOSE)) { |
||||||
|
ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); |
||||||
|
} |
||||||
|
|
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num) |
||||||
|
{ |
||||||
|
char buf[12]; |
||||||
|
itoa(num, buf, 10); |
||||||
|
return tpl_kv_add(head, key, buf); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num) |
||||||
|
{ |
||||||
|
char buf[21]; |
||||||
|
sprintf(buf, "%"PRIi64, num); |
||||||
|
return tpl_kv_add(head, key, buf); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h) |
||||||
|
{ |
||||||
|
char buf[IP4ADDR_STRLEN_MAX]; |
||||||
|
ip4_addr_t addr; |
||||||
|
addr.addr = lwip_htonl(ip4h); |
||||||
|
ip4addr_ntoa_r(&addr, buf, IP4ADDR_STRLEN_MAX); |
||||||
|
|
||||||
|
return tpl_kv_add(head, key, buf); |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst) |
||||||
|
{ |
||||||
|
ESP_LOGD(TAG, "kv add subs %s := %s", key, subst); |
||||||
|
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); |
||||||
|
if (entry == NULL) return ESP_ERR_NO_MEM; |
||||||
|
|
||||||
|
assert(strlen(key) < TPL_KV_KEY_LEN); |
||||||
|
assert(strlen(subst) < TPL_KV_SUBST_LEN); |
||||||
|
|
||||||
|
strncpy(entry->key, key, TPL_KV_KEY_LEN); |
||||||
|
entry->key[TPL_KV_KEY_LEN - 1] = 0; |
||||||
|
|
||||||
|
strncpy(entry->subst, subst, TPL_KV_SUBST_LEN - 1); |
||||||
|
entry->subst[TPL_KV_KEY_LEN - 1] = 0; |
||||||
|
|
||||||
|
SLIST_INSERT_HEAD(head, entry, link); |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst) |
||||||
|
{ |
||||||
|
ESP_LOGD(TAG, "kv add subs %s := (heap str)", key); |
||||||
|
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); |
||||||
|
if (entry == NULL) return ESP_ERR_NO_MEM; |
||||||
|
|
||||||
|
assert(strlen(key) < TPL_KV_KEY_LEN); |
||||||
|
|
||||||
|
strncpy(entry->key, key, TPL_KV_KEY_LEN); |
||||||
|
entry->key[TPL_KV_KEY_LEN - 1] = 0; |
||||||
|
|
||||||
|
entry->subst_heap = subst; |
||||||
|
|
||||||
|
SLIST_INSERT_HEAD(head, entry, link); |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...) |
||||||
|
{ |
||||||
|
ESP_LOGD(TAG, "kv printf %s := %s", key, format); |
||||||
|
struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry)); |
||||||
|
if (entry == NULL) return ESP_ERR_NO_MEM; |
||||||
|
|
||||||
|
assert(strlen(key) < TPL_KV_KEY_LEN); |
||||||
|
|
||||||
|
strncpy(entry->key, key, TPL_KV_KEY_LEN); |
||||||
|
entry->key[TPL_KV_KEY_LEN - 1] = 0; |
||||||
|
|
||||||
|
va_list list; |
||||||
|
va_start(list, format); |
||||||
|
vsnprintf(entry->subst, TPL_KV_SUBST_LEN, format, list); |
||||||
|
va_end(list); |
||||||
|
entry->subst[TPL_KV_KEY_LEN - 1] = 0; |
||||||
|
|
||||||
|
SLIST_INSERT_HEAD(head, entry, link); |
||||||
|
return ESP_OK; |
||||||
|
} |
||||||
|
|
||||||
|
void tpl_kv_free(struct tpl_kv_list *head) |
||||||
|
{ |
||||||
|
struct tpl_kv_entry *item, *next; |
||||||
|
SLIST_FOREACH_SAFE(item, head, link, next) { |
||||||
|
if (item->subst_heap) { |
||||||
|
free(item->subst_heap); |
||||||
|
item->subst_heap = NULL; |
||||||
|
} |
||||||
|
|
||||||
|
free(item); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head) |
||||||
|
{ |
||||||
|
httpd_resp_set_type(req, "text/plain; charset=utf-8"); |
||||||
|
|
||||||
|
#define BUF_CAP 512 |
||||||
|
char *buf_head = malloc(BUF_CAP); |
||||||
|
if (!buf_head) { |
||||||
|
ESP_LOGE(TAG, "Malloc err"); |
||||||
|
return ESP_FAIL; |
||||||
|
} |
||||||
|
char *buf = buf_head; |
||||||
|
size_t cap = BUF_CAP; |
||||||
|
struct tpl_kv_entry *entry; |
||||||
|
|
||||||
|
// GCC nested function
|
||||||
|
esp_err_t send_part() { |
||||||
|
esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); |
||||||
|
buf = buf_head; // buf is assigned to buf head
|
||||||
|
cap = BUF_CAP; |
||||||
|
if (suc != ESP_OK) { |
||||||
|
ESP_LOGE(TAG, "Error sending buffer"); |
||||||
|
free(buf_head); |
||||||
|
httpd_resp_send_chunk(req, NULL, 0); |
||||||
|
} |
||||||
|
return suc; |
||||||
|
} |
||||||
|
|
||||||
|
SLIST_FOREACH(entry, head, link) { |
||||||
|
while(NULL == (buf = append(buf, entry->key, &cap))) ESP_TRY(send_part()); |
||||||
|
while(NULL == (buf = append(buf, "\x1f", &cap))) ESP_TRY(send_part()); |
||||||
|
|
||||||
|
if (entry->subst_heap) { |
||||||
|
if (strlen(entry->subst_heap) >= BUF_CAP) { |
||||||
|
// send what we have
|
||||||
|
ESP_TRY(send_part()); |
||||||
|
esp_err_t suc = httpd_resp_send_chunk(req, entry->subst_heap, -1); |
||||||
|
if (suc != ESP_OK) { |
||||||
|
ESP_LOGE(TAG, "Error sending buffer"); |
||||||
|
free(buf_head); |
||||||
|
httpd_resp_send_chunk(req, NULL, 0); |
||||||
|
} |
||||||
|
} else { |
||||||
|
while (NULL == (buf = append(buf, entry->subst_heap, &cap))) ESP_TRY(send_part()); |
||||||
|
} |
||||||
|
} else { |
||||||
|
while(NULL == (buf = append(buf, entry->subst, &cap))) ESP_TRY(send_part()); |
||||||
|
} |
||||||
|
|
||||||
|
if (entry->link.sle_next) { |
||||||
|
while(NULL == (buf = append(buf, "\x1e", &cap))) ESP_TRY(send_part()); |
||||||
|
} |
||||||
|
} |
||||||
|
// send leftovers
|
||||||
|
if (buf != buf_head) { |
||||||
|
esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); |
||||||
|
if (suc != ESP_OK) { |
||||||
|
ESP_LOGE(TAG, "Error sending buffer"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Commit
|
||||||
|
httpd_resp_send_chunk(req, NULL, 0); |
||||||
|
free(buf_head); |
||||||
|
return ESP_OK; |
||||||
|
} |
@ -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
|
After Width: | Height: | Size: 2.0 KiB |
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 |
||||||
|
); |
@ -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…
Reference in new issue