Browse Source

add webserver components, some webserver fices and improvements in templating, add real time history chart

Ondřej Hruška 1 year ago
parent
commit
ef7866a29f
Signed by: Ondřej Hruška <ondra@ondrovo.com> GPG key ID: 2C5FD5035250423D
50 changed files with 3594 additions and 2 deletions
  1. 8 0
      components/common_utils/CMakeLists.txt
  2. 2 0
      components/common_utils/README.txt
  3. 3 0
      components/common_utils/component.mk
  4. 75 0
      components/common_utils/include/common_utils/base16.h
  5. 131 0
      components/common_utils/include/common_utils/datetime.h
  6. 19 0
      components/common_utils/include/common_utils/hexdump.h
  7. 101 0
      components/common_utils/include/common_utils/utils.h
  8. 62 0
      components/common_utils/src/base16.c
  9. 85 0
      components/common_utils/src/common_utils.c
  10. 110 0
      components/common_utils/src/datetime.c
  11. 72 0
      components/common_utils/src/hexdump.c
  12. 9 0
      components/fileserver/CMakeLists.txt
  13. 2 0
      components/fileserver/README.txt
  14. 3 0
      components/fileserver/component.mk
  15. 51 0
      components/fileserver/include/fileserver/embedded_files.h
  16. 227 0
      components/fileserver/include/fileserver/token_subs.h
  17. 29 0
      components/fileserver/readme/README.md
  18. 170 0
      components/fileserver/readme/rebuild_file_tables.php
  19. 22 0
      components/fileserver/src/embedded_files.c
  20. 619 0
      components/fileserver/src/token_subs.c
  21. 9 0
      components/httpd_utils/CMakeLists.txt
  22. 4 0
      components/httpd_utils/README.txt
  23. 3 0
      components/httpd_utils/component.mk
  24. 32 0
      components/httpd_utils/include/httpd_utils/captive.h
  25. 13 0
      components/httpd_utils/include/httpd_utils/fd_to_ipv4.h
  26. 16 0
      components/httpd_utils/include/httpd_utils/redirect.h
  27. 55 0
      components/httpd_utils/include/httpd_utils/session.h
  28. 77 0
      components/httpd_utils/include/httpd_utils/session_kvmap.h
  29. 100 0
      components/httpd_utils/include/httpd_utils/session_store.h
  30. 106 0
      components/httpd_utils/src/captive.c
  31. 42 0
      components/httpd_utils/src/fd_to_ipv4.c
  32. 20 0
      components/httpd_utils/src/redirect.c
  33. 181 0
      components/httpd_utils/src/session_kvmap.c
  34. 220 0
      components/httpd_utils/src/session_store.c
  35. 41 0
      components/httpd_utils/src/session_utils.c
  36. 10 0
      main/CMakeLists.txt
  37. 16 1
      main/analog.c
  38. 5 0
      main/analog.h
  39. 78 1
      main/app_main.c
  40. 54 0
      main/files/embed/chart.ignore.svg
  41. BIN
      main/files/embed/favicon.ico
  42. 137 0
      main/files/embed/index.html
  43. 15 0
      main/files/files_enum.c
  44. 13 0
      main/files/files_enum.h
  45. 163 0
      main/files/rebuild_file_tables.php
  46. 70 0
      main/utils.c
  47. 76 0
      main/utils.h
  48. 12 0
      main/version.h
  49. 212 0
      main/web/websrv.c
  50. 14 0
      main/web/websrv.h

+ 8 - 0
components/common_utils/CMakeLists.txt View File

@@ -0,0 +1,8 @@
1
+set(COMPONENT_ADD_INCLUDEDIRS include)
2
+
3
+set(COMPONENT_SRCDIRS
4
+        "src")
5
+
6
+#set(COMPONENT_REQUIRES)
7
+
8
+register_component()

+ 2 - 0
components/common_utils/README.txt View File

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

+ 3 - 0
components/common_utils/component.mk View File

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

+ 75 - 0
components/common_utils/include/common_utils/base16.h View File

@@ -0,0 +1,75 @@
1
+/*
2
+ * Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
3
+ *
4
+ * This program is free software; you can redistribute it and/or
5
+ * modify it under the terms of the GNU General Public License as
6
+ * published by the Free Software Foundation; either version 2 of the
7
+ * License, or any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful, but
10
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12
+ * General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program; if not, write to the Free Software
16
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
+ */
18
+
19
+#ifndef BASE16_H_
20
+#define BASE16_H_
21
+
22
+#include <stdint.h>
23
+#include <string.h>
24
+
25
+/**
26
+ * Calculate length of base16-encoded data
27
+ * @param raw_len Raw data length
28
+ * @return Encoded string length (excluding NUL)
29
+ */
30
+static inline size_t base16_encoded_len(size_t raw_len) {
31
+	return (2 * raw_len);
32
+}
33
+
34
+/**
35
+ * Calculate maximum length of base16-decoded string
36
+ * @param encoded Encoded string
37
+ * @return Maximum length of raw data
38
+ */
39
+static inline size_t base16_decoded_max_len(const char *encoded) {
40
+	return ((strlen(encoded) + 1) / 2);
41
+}
42
+
43
+/**
44
+ * Base16-encode data
45
+ *
46
+ * The buffer must be the correct length for the encoded string.  Use
47
+ * something like
48
+ *
49
+ *     char buf[ base16_encoded_len ( len ) + 1 ];
50
+ *
51
+ * (the +1 is for the terminating NUL) to provide a buffer of the
52
+ * correct size.
53
+ *
54
+ * @param raw Raw data
55
+ * @param len Length of raw data
56
+ * @param encoded Buffer for encoded string
57
+ */
58
+extern void base16_encode(uint8_t *raw, size_t len, char *encoded);
59
+
60
+/**
61
+ * Base16-decode data
62
+ *
63
+ * The buffer must be large enough to contain the decoded data.  Use
64
+ * something like
65
+ *
66
+ *     char buf[ base16_decoded_max_len ( encoded ) ];
67
+ *
68
+ * to provide a buffer of the correct size.
69
+ * @param encoded Encoded string
70
+ * @param raw Raw data
71
+ * @return Length of raw data, or negative error
72
+ */
73
+extern int base16_decode(const char *encoded, uint8_t *raw);
74
+
75
+#endif /* BASE16_H_ */

+ 131 - 0
components/common_utils/include/common_utils/datetime.h View File

@@ -0,0 +1,131 @@
1
+/**
2
+ * TODO file description
3
+ * 
4
+ * Created on 2019/09/13.
5
+ */
6
+
7
+#ifndef CSPEMU_DATETIME_H
8
+#define CSPEMU_DATETIME_H
9
+
10
+#include <stdbool.h>
11
+#include <stdint.h>
12
+
13
+enum weekday {
14
+    MONDAY = 1,
15
+    TUESDAY,
16
+    WEDNESDAY,
17
+    THURSDAY,
18
+    FRIDAY,
19
+    SATURDAY,
20
+    SUNDAY
21
+};
22
+_Static_assert(MONDAY==1, "enum weekday numbering Mon");
23
+_Static_assert(SUNDAY==7, "enum weekday numbering Sun");
24
+
25
+enum month {
26
+    JANUARY = 1,
27
+    FEBRUARY,
28
+    MARCH,
29
+    APRIL,
30
+    MAY,
31
+    JUNE,
32
+    JULY,
33
+    AUGUST,
34
+    SEPTEMBER,
35
+    OCTOBER,
36
+    NOVEMBER,
37
+    DECEMBER
38
+};
39
+_Static_assert(JANUARY==1, "enum month numbering Jan");
40
+_Static_assert(DECEMBER==12, "enum month numbering Dec");
41
+
42
+/** Abbreviated weekday names */
43
+extern const char *DT_WKDAY_NAMES[];
44
+/** Full-length weekday names */
45
+extern const char *DT_WKDAY_NAMES_FULL[];
46
+/** Abbreviated month names */
47
+extern const char *DT_MONTH_NAMES[];
48
+/** Full-length month names */
49
+extern const char *DT_MONTH_NAMES_FULL[];
50
+
51
+typedef struct datetime {
52
+    uint16_t year;
53
+    enum month month;
54
+    uint8_t day;
55
+    uint8_t hour;
56
+    uint8_t min;
57
+    uint8_t sec;
58
+    enum weekday wkday; // 1=monday
59
+} datetime_t;
60
+
61
+// Templates for printf
62
+#define DT_FORMAT_DATE   "%d/%d/%d"
63
+#define DT_SUBS_DATE(dt) (dt).year, (dt).month, (dt).day
64
+
65
+#define DT_FORMAT_TIME   "%d:%02d:%02d"
66
+#define DT_SUBS_TIME(dt) (dt).hour, (dt).min, (dt).sec
67
+
68
+#define DT_FORMAT_DATE_WK   DT_FORMAT_WK " " DT_FORMAT_DATE
69
+#define DT_SUBS_DATE_WK(dt) DT_SUBS_WK(dt), DT_SUBS_DATE(dt)
70
+
71
+#define DT_FORMAT_WK   "%s"
72
+#define DT_SUBS_WK(dt) DT_WKDAY_NAMES[(dt).wkday]
73
+
74
+#define DT_FORMAT_DATE_TIME   DT_FORMAT_DATE " " DT_FORMAT_TIME
75
+#define DT_SUBS_DATE_TIME(dt) DT_SUBS_DATE(dt), DT_SUBS_TIME(dt)
76
+
77
+#define DT_FORMAT   DT_FORMAT_DATE_WK " " DT_FORMAT_TIME
78
+#define DT_SUBS(dt) DT_SUBS_DATE_WK(dt), DT_SUBS_TIME(dt)
79
+
80
+// base century for two-digit year conversions
81
+#define DT_CENTURY 2000
82
+// start year for weekday computation
83
+#define DT_START_YEAR 2019
84
+// January 1st weekday of DT_START_YEAR
85
+#define DT_START_WKDAY TUESDAY
86
+// max date supported by 2-digit year RTC counters (it can't check Y%400==0 with only two digits)
87
+#define DT_END_YEAR 2399
88
+
89
+typedef union __attribute__((packed)) {
90
+    struct __attribute__((packed)) {
91
+        uint8_t ones : 4;
92
+        uint8_t tens : 4;
93
+    };
94
+    uint8_t byte;
95
+} bcd_t;
96
+_Static_assert(sizeof(bcd_t) == 1, "Bad bcd_t len");
97
+
98
+/** Check if a year is leap */
99
+static inline bool is_leap_year(int year)
100
+{
101
+    return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
102
+}
103
+
104
+/**
105
+ * Check if a datetime could be valid (ignores leap years)
106
+ *
107
+ * @param[in] dt
108
+ * @return basic validations passed
109
+ */
110
+bool datetime_is_valid(const datetime_t *dt);
111
+
112
+/**
113
+ * Set weekday based on a date in a given datetime
114
+ *
115
+ * @param[in,out] dt
116
+ * @return success
117
+ */
118
+bool datetime_set_weekday(datetime_t *dt);
119
+
120
+/**
121
+ * Get weekday for given a date
122
+ *
123
+ * @param year - year number
124
+ * @param month - 1-based month number
125
+ * @param day - 1-based day number
126
+ * @return weekday
127
+ */
128
+enum weekday date_weekday(uint16_t year, enum month month, uint8_t day);
129
+
130
+
131
+#endif //CSPEMU_DATETIME_H

+ 19 - 0
components/common_utils/include/common_utils/hexdump.h View File

@@ -0,0 +1,19 @@
1
+/**
2
+ * @file
3
+ * @brief A simple way of dumping memory to a hex output
4
+ *
5
+ * \addtogroup Hexdump
6
+ *
7
+ * @{
8
+ */
9
+
10
+
11
+#include <stdio.h>
12
+
13
+#define HEX_DUMP_LINE_BUFF_SIZ  16
14
+
15
+extern void hex_dump(FILE * fp,void *src, int len);
16
+extern void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len);
17
+/**
18
+ * }@
19
+ */

+ 101 - 0
components/common_utils/include/common_utils/utils.h View File

@@ -0,0 +1,101 @@
1
+/**
2
+ * General purpose, platform agnostic, reusable utils
3
+ */
4
+
5
+#ifndef COMMON_UTILS_UTILS_H
6
+#define COMMON_UTILS_UTILS_H
7
+
8
+#include <stdbool.h>
9
+#include <stdint.h>
10
+
11
+#include "base16.h"
12
+#include "datetime.h"
13
+#include "hexdump.h"
14
+
15
+/** Convert a value to BCD struct */
16
+static inline bcd_t num2bcd(uint8_t value)
17
+{
18
+    return (bcd_t) {.ones=value % 10, .tens=value / 10};
19
+}
20
+
21
+/** Convert unpacked BCD to value */
22
+static inline uint8_t bcd2num(uint8_t tens, uint8_t ones)
23
+{
24
+    return tens * 10 + ones;
25
+}
26
+
27
+/**
28
+ * Append to a buffer.
29
+ *
30
+ * In case the buffer capacity is reached, it is reverted to the previous length (by replacing the NUL byte)
31
+ * and NULL is returned.
32
+ *
33
+ * @param buf - buffer position to append at; if NULL is given, the function immediately returns NULL.
34
+ * @param appended - string to append
35
+ * @param pcap - pointer to a capacity variable
36
+ * @return the new end of the string (null byte); use as 'buf' for following appends
37
+ */
38
+char *append(char *buf, const char *appended, size_t *pcap);
39
+
40
+/**
41
+ * Append, re-allocating as needed.
42
+ *
43
+ * @param head - pointer to heap buffer head, may be updated on realloc.
44
+ * @param size - string size pointer, may be updated on realloc.
45
+ * @param appended - string to append
46
+ * @return false on alloc error
47
+ */
48
+bool append_realloc(char **head, size_t *cap, const char *appended);
49
+
50
+/**
51
+ * Test if a file descriptor is valid (e.g. when cleaning up after a failed select)
52
+ *
53
+ * @param fd - file descriptor number
54
+ * @return is valid
55
+ */
56
+bool fd_is_valid(int fd);
57
+
58
+/**
59
+ * parse user-provided string as boolean
60
+ *
61
+ * @param str
62
+ * @return is true
63
+ */
64
+bool parse_boolean_arg(const char *str);
65
+
66
+/**
67
+ * complementary function to parse_boolean_arg() that matches strings
68
+ * meaning 'false'. Can be used together with the positive version
69
+ * in case there can be other values as well.
70
+ *
71
+ * @param str
72
+ * @return is false
73
+ */
74
+bool parse_boolean_arg_false(const char *str);
75
+
76
+/** Check equality of two strings; returns bool */
77
+#define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0)
78
+
79
+/** Check prefix equality of two strings; returns bool */
80
+#define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0)
81
+
82
+/** Check if a string starts with a substring; returns bool */
83
+#define strstarts(a, b) strneq((a), (b), (int)strlen((b)))
84
+
85
+#ifndef MIN
86
+/** Get min of two numbers */
87
+#define MIN(a, b) ((a) > (b) ? (b) : (a))
88
+#endif
89
+
90
+#ifndef MAX
91
+/** Get max of two values */
92
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
93
+#endif
94
+
95
+#ifndef STR
96
+#define STR_HELPER(x) #x
97
+/** Stringify a token */
98
+#define STR(x) STR_HELPER(x)
99
+#endif
100
+
101
+#endif //COMMON_UTILS_UTILS_H

+ 62 - 0
components/common_utils/src/base16.c View File

@@ -0,0 +1,62 @@
1
+/*
2
+ * Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
3
+ *
4
+ * This program is free software; you can redistribute it and/or
5
+ * modify it under the terms of the GNU General Public License as
6
+ * published by the Free Software Foundation; either version 2 of the
7
+ * License, or any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful, but
10
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12
+ * General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program; if not, write to the Free Software
16
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
+ */
18
+
19
+#include <stdint.h>
20
+#include <stdlib.h>
21
+#include <string.h>
22
+#include <stdio.h>
23
+#include <esp_log.h>
24
+
25
+static const char *TAG = "base16";
26
+
27
+void base16_encode(uint8_t *raw, size_t len, char *encoded) {
28
+	uint8_t *raw_bytes = raw;
29
+	char *encoded_bytes = encoded;
30
+	size_t remaining = len;
31
+
32
+	for (; remaining--; encoded_bytes += 2)
33
+		snprintf(encoded_bytes, 3, "%02X", *(raw_bytes++));
34
+
35
+}
36
+
37
+int base16_decode(const char *encoded, uint8_t *raw) {
38
+	const char *encoded_bytes = encoded;
39
+	uint8_t *raw_bytes = raw;
40
+	char buf[3];
41
+	char *endp;
42
+	size_t len;
43
+
44
+	while (encoded_bytes[0]) {
45
+		if (!encoded_bytes[1]) {
46
+			ESP_LOGE(TAG, "Base16-encoded string \"%s\" has invalid length\n",
47
+					encoded);
48
+			return -22;
49
+		}
50
+		memcpy(buf, encoded_bytes, 2);
51
+		buf[2] = '\0';
52
+		*(raw_bytes++) = strtoul(buf, &endp, 16);
53
+		if (*endp != '\0') {
54
+			ESP_LOGE(TAG,"Base16-encoded string \"%s\" has invalid byte \"%s\"\n",
55
+					encoded, buf);
56
+			return -22;
57
+		}
58
+		encoded_bytes += 2;
59
+	}
60
+	len = (raw_bytes - raw);
61
+	return (len);
62
+}

+ 85 - 0
components/common_utils/src/common_utils.c View File

@@ -0,0 +1,85 @@
1
+#include <stdint.h>
2
+#include <string.h>
3
+#include <stdbool.h>
4
+#include <fcntl.h>
5
+#include <errno.h>
6
+#include <assert.h>
7
+
8
+#include "common_utils/utils.h"
9
+
10
+char *append(char *buf, const char *appended, size_t *pcap)
11
+{
12
+    char c;
13
+    char *buf0 = buf;
14
+    size_t cap = *pcap;
15
+
16
+    if (buf0 == NULL) return NULL;
17
+    if (appended == NULL || appended[0] == 0) return buf0;
18
+
19
+    if (*pcap < strlen(appended)+1) {
20
+        return NULL;
21
+    }
22
+
23
+    while (cap > 1 && 0 != (c = *appended++)) {
24
+        *buf++ = c;
25
+        cap--;
26
+    }
27
+    assert(cap > 0);
28
+
29
+    *pcap = cap;
30
+    *buf = 0;
31
+    return buf;
32
+}
33
+
34
+bool append_realloc(char **head, size_t *cap, const char *appended) {
35
+    if (!head) return NULL;
36
+    if (!*head) return NULL;
37
+    if (!cap) return NULL;
38
+    if (!appended) return NULL;
39
+
40
+    size_t cursize = strlen(*head);
41
+    size_t needed = strlen(appended) + 1;
42
+    size_t remains = *cap - cursize;
43
+
44
+    if (remains < needed) {
45
+        size_t need_extra = needed - remains;
46
+        size_t newsize = *cap + need_extra;
47
+        char *new = realloc(*head, newsize);
48
+        if (!new) return false;
49
+        *head = new;
50
+        *cap = newsize;
51
+    }
52
+
53
+    strcpy(*head + cursize, appended);
54
+    return true;
55
+}
56
+
57
+bool fd_is_valid(int fd)
58
+{
59
+    return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
60
+}
61
+
62
+bool parse_boolean_arg(const char *str)
63
+{
64
+    if (0 == strcasecmp(str, "on")) return true;
65
+    if (0 == strcmp(str, "1")) return true;
66
+    if (0 == strcasecmp(str, "yes")) return true;
67
+    if (0 == strcasecmp(str, "enable")) return true;
68
+    if (0 == strcasecmp(str, "en")) return true;
69
+    if (0 == strcasecmp(str, "y")) return true;
70
+    if (0 == strcasecmp(str, "a")) return true;
71
+
72
+    return false;
73
+}
74
+
75
+bool parse_boolean_arg_false(const char *str)
76
+{
77
+    if (0 == strcasecmp(str, "off")) return true;
78
+    if (0 == strcmp(str, "0")) return true;
79
+    if (0 == strcasecmp(str, "no")) return true;
80
+    if (0 == strcasecmp(str, "disable")) return true;
81
+    if (0 == strcasecmp(str, "dis")) return true;
82
+    if (0 == strcasecmp(str, "n")) return true;
83
+
84
+    return false;
85
+}

+ 110 - 0
components/common_utils/src/datetime.c View File

@@ -0,0 +1,110 @@
1
+
2
+#include <stdint.h>
3
+#include <stdbool.h>
4
+#include <stddef.h>
5
+#include "common_utils/datetime.h"
6
+
7
+const char *DT_WKDAY_NAMES[] = {
8
+    [MONDAY] = "Mon",
9
+    [TUESDAY] = "Tue",
10
+    [WEDNESDAY] = "Wed",
11
+    [THURSDAY] = "Thu",
12
+    [FRIDAY] = "Fri",
13
+    [SATURDAY] = "Sat",
14
+    [SUNDAY] = "Sun"
15
+};
16
+
17
+const char *DT_WKDAY_NAMES_FULL[] = {
18
+    [MONDAY] = "Monday",
19
+    [TUESDAY] = "Tuesday",
20
+    [WEDNESDAY] = "Wednesday",
21
+    [THURSDAY] = "Thursday",
22
+    [FRIDAY] = "Friday",
23
+    [SATURDAY] = "Saturday",
24
+    [SUNDAY] = "Sunday"
25
+};
26
+
27
+const char *DT_MONTH_NAMES[] = {
28
+    [JANUARY] = "Jan",
29
+    [FEBRUARY] = "Feb",
30
+    [MARCH] = "Mar",
31
+    [APRIL] = "Apr",
32
+    [MAY] = "May",
33
+    [JUNE] = "Jun",
34
+    [JULY] = "Jul",
35
+    [AUGUST] = "Aug",
36
+    [SEPTEMBER] = "Sep",
37
+    [OCTOBER] = "Oct",
38
+    [NOVEMBER] = "Nov",
39
+    [DECEMBER] = "Dec"
40
+};
41
+
42
+const char *DT_MONTH_NAMES_FULL[] = {
43
+    [JANUARY] = "January",
44
+    [FEBRUARY] = "February",
45
+    [MARCH] = "March",
46
+    [APRIL] = "April",
47
+    [MAY] = "May",
48
+    [JUNE] = "June",
49
+    [JULY] = "July",
50
+    [AUGUST] = "August",
51
+    [SEPTEMBER] = "September",
52
+    [OCTOBER] = "October",
53
+    [NOVEMBER] = "November",
54
+    [DECEMBER] = "December"
55
+};
56
+
57
+static const uint16_t MONTH_LENGTHS[] = { /* 1-based, normal year */
58
+    [JANUARY]=31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
59
+};
60
+_Static_assert(sizeof(MONTH_LENGTHS) / sizeof(uint16_t) == 13, "Months array length");
61
+
62
+static const uint16_t MONTH_YEARDAYS[] = { /* 1-based */
63
+    [JANUARY]=0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 // // days until 1st of month
64
+};
65
+_Static_assert(sizeof(MONTH_YEARDAYS) / sizeof(uint16_t) == 13, "Months array length");
66
+
67
+_Static_assert(MONDAY < SUNDAY, "Weekday ordering");
68
+
69
+bool datetime_is_valid(const datetime_t *dt)
70
+{
71
+    if (dt == NULL) return false;
72
+
73
+    // check month first to avoid out-of-bounds read from the MONTH_LENGTHS table
74
+    if (!(dt->month >= JANUARY && dt->month <= DECEMBER)) return false;
75
+
76
+    int monthlen = MONTH_LENGTHS[dt->month];
77
+    if (dt->month == FEBRUARY && is_leap_year(dt->year)) {
78
+        monthlen = 29;
79
+    }
80
+
81
+    return dt->sec < 60 &&
82
+           dt->min < 60 &&
83
+           dt->hour < 24 &&
84
+           dt->wkday >= MONDAY &&
85
+           dt->wkday <= SUNDAY &&
86
+           dt->year >= DT_START_YEAR &&
87
+           dt->year <= DT_END_YEAR &&
88
+           dt->day >= 1 &&
89
+           dt->day <= monthlen;
90
+}
91
+
92
+bool datetime_set_weekday(datetime_t *dt)
93
+{
94
+    dt->wkday = MONDAY; // prevent the validator func erroring out on invalid weekday
95
+    if (!datetime_is_valid(dt)) return false;
96
+    dt->wkday = date_weekday(dt->year, dt->month, dt->day);
97
+    return true;
98
+}
99
+
100
+enum weekday date_weekday(uint16_t year, enum month month, uint8_t day)
101
+{
102
+    uint16_t days = (DT_START_WKDAY - MONDAY) + (year - DT_START_YEAR) * 365 + MONTH_YEARDAYS[month] + (day - 1);
103
+
104
+    for (uint16_t i = DT_START_YEAR; i <= year; i++) {
105
+        if (is_leap_year(i) && (i < year || month > FEBRUARY)) days++;
106
+    }
107
+
108
+    return MONDAY + days % 7;
109
+}
110
+

+ 72 - 0
components/common_utils/src/hexdump.c View File

@@ -0,0 +1,72 @@
1
+/*
2
+ * util.c
3
+ *
4
+ *  Created on: Aug 12, 2009
5
+ *      Author: johan
6
+ */
7
+
8
+// adapted from libgomspace
9
+
10
+#include <string.h>
11
+#include <stdio.h>
12
+#include "common_utils/hexdump.h"
13
+
14
+
15
+//! Dump memory to debugging output
16
+/**
17
+ * Dumps a chunk of memory to the screen
18
+ */
19
+void hex_dump(FILE * fp, void *src, int len) {
20
+	int i, j=0, k;
21
+	char text[17];
22
+
23
+	text[16] = '\0';
24
+	//printf("Hex dump:\r\n");
25
+	fprintf(fp, "%p : ", src);
26
+	for(i=0; i<len; i++) {
27
+		j++;
28
+		fprintf(fp, "%02X ", ((volatile unsigned char *)src)[i]);
29
+		if(j == 8)
30
+			fputc(' ', fp);
31
+		if(j == 16) {
32
+			j = 0;
33
+			memcpy(text, &((char *)src)[i-15], 16);
34
+			for(k=0; k<16; k++) {
35
+				if((text[k] < 32) || (text[k] > 126)) {
36
+					text[k] = '.';
37
+				}
38
+			}
39
+			fprintf(fp, " |%s|\n\r", text);
40
+			if(i<len-1) {
41
+				fprintf(fp, "%p : ", src+i+1);
42
+			}
43
+		}
44
+	}
45
+	if (i % 16)
46
+		fprintf(fp, "\r\n");
47
+}
48
+
49
+void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len)
50
+{
51
+  unsigned i;
52
+
53
+  fprintf(fp, "%0*x", addr_size, pos);
54
+  for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ; i++)
55
+  {
56
+    if (!(i % 8))
57
+      fputc(' ', fp);
58
+    if (i < len)
59
+      fprintf(fp, " %02x", (unsigned char)line[i]);
60
+    else
61
+	  fputs("   ", fp);
62
+  }
63
+  fputs("  |", fp);
64
+  for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ && i < len; i++)
65
+  {
66
+    if (line[i] >= 32 && line[i] <= 126)
67
+      fprintf(fp, "%c", (unsigned char)line[i]);
68
+    else
69
+		fputc('.', fp);
70
+  }
71
+  fputs("|\r\n", fp);
72
+}

+ 9 - 0
components/fileserver/CMakeLists.txt View File

@@ -0,0 +1,9 @@
1
+set(COMPONENT_ADD_INCLUDEDIRS
2
+        "include")
3
+
4
+set(COMPONENT_SRCDIRS
5
+        "src")
6
+
7
+set(COMPONENT_REQUIRES tcpip_adapter esp_http_server httpd_utils common_utils)
8
+
9
+register_component()

+ 2 - 0
components/fileserver/README.txt View File

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

+ 3 - 0
components/fileserver/component.mk View File

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

+ 51 - 0
components/fileserver/include/fileserver/embedded_files.h View File

@@ -0,0 +1,51 @@
1
+//
2
+// Created on 2018/10/17 by Ondrej Hruska <ondra@ondrovo.com>
3
+//
4
+
5
+#ifndef FBNODE_EMBEDDED_FILES_H
6
+#define FBNODE_EMBEDDED_FILES_H
7
+
8
+#include <stdint.h>
9
+#include <esp_err.h>
10
+#include <stdbool.h>
11
+
12
+struct embedded_file_info {
13
+    const uint8_t * start;
14
+    const uint8_t * end;
15
+    const char * name;
16
+    const char * mime;
17
+};
18
+
19
+enum file_access_level {
20
+    /** Public = file accessed by a wildcard route */
21
+    FILE_ACCESS_PUBLIC = 0,
22
+    /** Protected = file included in a template or explicitly specified in a route */
23
+    FILE_ACCESS_PROTECTED = 1,
24
+    /** Files protected against read-out */
25
+    FILE_ACCESS_PRIVATE = 2,
26
+};
27
+
28
+extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[];
29
+extern const size_t EMBEDDED_FILE_LOOKUP_LEN;
30
+
31
+/**
32
+ * Find an embedded file by its name.
33
+ * 
34
+ * This function is weak. It crawls the EMBEDDED_FILE_LOOKUP table and checks for exact match, also
35
+ * testing with www_get_static_file_access_check if the access is allowed.
36
+ *
37
+ * @param name - file name
38
+ * @param access - access level (public - wildcard fallthrough, protected - files for the server, loaded explicitly)
39
+ * @param[out] file - the file struct is stored here if found, unchanged if not found.
40
+ * @return status code
41
+ */
42
+esp_err_t www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file);
43
+
44
+/**
45
+ * Check file access permission (if using the default www_get_static_file implementation).
46
+ * 
47
+ * This function is weak. The default implementation returns always true.
48
+ */
49
+bool www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access);
50
+
51
+#endif //FBNODE_EMBEDDED_FILES_H

+ 227 - 0
components/fileserver/include/fileserver/token_subs.h View File

@@ -0,0 +1,227 @@
1
+//
2
+// This module implements token substitution in files served by the server.
3
+//
4
+// Tokens are in the form {token}, or {escape:token}, where escape can be:
5
+// - h ... html escape (plain text in a html file, attribute value)
6
+// - j ... js escape (for use in JS strings)
7
+//
8
+// When no escape is specified, the token substitution is written verbatim into the response.
9
+//
10
+//   var foo = "{j:foo}";
11
+//   <input value="{h:old_value}">
12
+//   {generated-html-goes-here}
13
+//
14
+// Token can be made optional by adding '?' at the end (this can't be used for includes).
15
+// Such token then simply becomes empty string when not substituted, as opposed to being included in the page verbatim.
16
+//
17
+//   <input value="{h:old_value?}">
18
+//
19
+// token names can contain alnum, dash, period and underscore, and are case sensitive.
20
+//
21
+//
22
+// It is further possible to include a static file with optional key-value replacements. These serve as defaults.
23
+//
24
+//   {@_subfile.html}
25
+//   {@_subfile.html|key=value lalala}
26
+//   {@_subfile.html|key=value lalala|other=value}
27
+//
28
+// File inclusion can be nested, and the files can use replacement tokens as specified by the include statement
29
+//
30
+// Created on 2019/01/24 by Ondrej Hruska <ondra@ondrovo.com>
31
+//
32
+
33
+#ifndef FBNODE_TOKEN_SUBS_H
34
+#define FBNODE_TOKEN_SUBS_H
35
+
36
+#include "embedded_files.h"
37
+#include <rom/queue.h>
38
+#include <esp_err.h>
39
+#include <esp_http_server.h>
40
+
41
+/** Max length of a token buffer (must suffice for all included filenames) */
42
+#define MAX_TOKEN_LEN 32
43
+
44
+/** Max length of a key-value substitution when using tpl_kv_replacer;
45
+ * This is also used internally for in-line replacements in file imports. */
46
+#define TPL_KV_KEY_LEN 24
47
+/** Max length of a substituion in tpl_kv_replacer */
48
+#define TPL_KV_SUBST_LEN 64
49
+
50
+/**
51
+ * Escape type - argument for httpd_resp_send_chunk_escaped()
52
+ */
53
+typedef enum {
54
+    TPL_ESCAPE_NONE = 0,
55
+    TPL_ESCAPE_HTML,
56
+    TPL_ESCAPE_JS,
57
+} tpl_escape_t;
58
+
59
+enum {
60
+    HTOPT_NONE = 0,
61
+    HTOPT_NO_HEADERS = 1 << 0,
62
+    HTOPT_NO_CLOSE = 1 << 1,
63
+    HTOPT_INCLUDE = HTOPT_NO_HEADERS|HTOPT_NO_CLOSE,
64
+};
65
+
66
+/**
67
+ * Send string using a given escaping scheme
68
+ *
69
+ * @param r
70
+ * @param buf - buf to send
71
+ * @param len - buf len, or HTTPD_RESP_USE_STRLEN
72
+ * @param escape - escaping type
73
+ * @return success
74
+ */
75
+esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape);
76
+
77
+/**
78
+ * Template substitution callback. Data shall be sent using httpd_resp_send_chunk_escaped().
79
+ *
80
+ * @param[in,out] context - user-defined page state data
81
+ * @param[in] token - replacement token
82
+ * @return ESP_OK if the token was substituted, ESP_ERR_NOT_FOUND if it is unknown, other errors on e.g. send failure
83
+ */
84
+typedef esp_err_t (*template_subst_t)(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape);
85
+
86
+/**
87
+ * Send a template file as a response. The content type from the file struct will be used.
88
+ *
89
+ * Use HTOPT_INCLUDE when used to embed a file inside a template.
90
+ *
91
+ * @param r - request
92
+ * @param file_index - file index in EMBEDDED_FILE_LOOKUP
93
+ * @param replacer - substitution callback, can be NULL if only includes are to be processed
94
+ * @param context - arbitrary context, will be passed to the replacer function; can be NULL
95
+ * @param opts - flag options (HTOPT_*)
96
+ */
97
+esp_err_t httpd_send_template_file(httpd_req_t *r, int file_index, template_subst_t replacer, void *context, uint32_t opts);
98
+
99
+/**
100
+ * Same as httpd_send_template_file, but using an `embedded_file_info` struct.
101
+ */
102
+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);
103
+
104
+/**
105
+ * Process and send a string template.
106
+ * The content-type header should be set beforehand, if different from the default (text/html).
107
+ *
108
+ * Use HTOPT_INCLUDE when used to embed a file inside a template.
109
+ *
110
+ * @param r - request
111
+ * @param template - template string (does not have to be terminated by a null byte)
112
+ * @param template_len - length of the template string; -1 to use strlen()
113
+ * @param replacer - substitution callback, can be NULL if only includes are to be processed
114
+ * @param context - arbitrary context, will be passed to the replacer function; can be NULL
115
+ * @param opts - flag options (HTOPT_*)
116
+ */
117
+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);
118
+
119
+/**
120
+ * Send a static file. This can be used to just send a file, or to embed a static template as a token substitution.
121
+ *
122
+ * Use HTOPT_INCLUDE when used to embed a file inside a template.
123
+ *
124
+ * Note: use httpd_resp_send_chunk_escaped() or httpd_resp_send_chunk() to send a plain string.
125
+ *
126
+ * @param r - request
127
+ * @param file_index - file index in EMBEDDED_FILE_LOOKUP
128
+ * @param escape - escape option
129
+ * @param opts - flag options (HTOPT_*)
130
+ * @return
131
+ */
132
+esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts);
133
+
134
+/**
135
+ * Same as httpd_send_template_file, but using an `embedded_file_info` struct.
136
+ */
137
+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);
138
+
139
+struct tpl_kv_entry {
140
+    char key[TPL_KV_KEY_LEN]; // copied here
141
+    char subst[TPL_KV_SUBST_LEN]; // copied here
142
+    char *subst_heap;
143
+    SLIST_ENTRY(tpl_kv_entry) link;
144
+};
145
+
146
+SLIST_HEAD(tpl_kv_list, tpl_kv_entry);
147
+
148
+/**
149
+ * key-value replacer that works with a dynamically allocated SLIST.
150
+ *
151
+ * @param r - request
152
+ * @param context - context - must be a pointer to `struct tpl_kv_list`
153
+ * @param token - token to replace
154
+ * @param escape - escape option
155
+ * @return OK/not found/other
156
+ */
157
+esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape);
158
+
159
+/**
160
+ * Add a pair into the substitutions list
161
+ *
162
+ * @param head - list head
163
+ * @param key - key, copied
164
+ * @param subst - value, copied
165
+ * @return success (fails if malloc failed)
166
+ */
167
+esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst);
168
+
169
+/**
170
+ * Add a heap-allocated string to the replacer.
171
+ *
172
+ * @param head - list head
173
+ * @param key - key, copied
174
+ * @param subst - value, copied
175
+ * @return success (fails if malloc failed)
176
+ */
177
+esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst);
178
+
179
+/**
180
+ * Convenience function that converts an IP address to string and adds it as a substitution
181
+ *
182
+ * @param head - list head
183
+ * @param key - key, copied
184
+ * @param ip4h - host order ipv4 address
185
+ * @return success
186
+ */
187
+esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h);
188
+
189
+/** add int as a substitution; key is copied */
190
+esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num);
191
+
192
+/** add long as a substitution; key is copied */
193
+esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num);
194
+
195
+/** add printf-formatted value; key is copied */
196
+esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...)
197
+    __attribute__((format(printf,3,4)));
198
+
199
+/**
200
+ * Init a substitutions list (on the stack)
201
+ *
202
+ * @return the list
203
+ */
204
+static inline struct tpl_kv_list tpl_kv_init(void)
205
+{
206
+    return (struct tpl_kv_list) {.slh_first = NULL};
207
+}
208
+
209
+/**
210
+ * Free the list (head is left alone because it was allocated on the stack)
211
+ * @param head
212
+ */
213
+void tpl_kv_free(struct tpl_kv_list *head);
214
+
215
+/**
216
+ * Send the map as an ASCII table separated by Record Separator (30) and Unit Separator (31).
217
+ * Content type is set to application/octet-stream.
218
+ *
219
+ * key 31 value 30
220
+ * key 31 value 30
221
+ * key 31 value
222
+ *
223
+ * @param req
224
+ */
225
+esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head);
226
+
227
+#endif //FBNODE_TOKEN_SUBS_H

+ 29 - 0
components/fileserver/readme/README.md View File

@@ -0,0 +1,29 @@
1
+Place the `rebuild_file_tables.php` script in a `files` subfolder of the main component.
2
+It requires PHP 7 to run. 
3
+
4
+This is what the setup should look like
5
+
6
+```
7
+main/files/embed/index.html
8
+main/files/rebuild_file_tables.php
9
+main/CMakeLists.txt
10
+main/main.c
11
+```
12
+
13
+Add this to your CMakeLists.txt before `register_component`:
14
+
15
+```
16
+#begin staticfiles
17
+#end staticfiles
18
+```
19
+
20
+The script will update CMakeLists.txt and generate `files_enum.c` and `files_enum.h` when run.
21
+
22
+```
23
+main/files/files_enum.h
24
+main/files/files_enum.c
25
+```
26
+
27
+Ensure `files/files_enum.c` is included in the build.
28
+
29
+`www_get_static_file()` is implemented as weak to let you provide custom access authentication logic.

+ 170 - 0
components/fileserver/readme/rebuild_file_tables.php View File

@@ -0,0 +1,170 @@
1
+#!/usr/bin/env php
2
+<?php
3
+// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
4
+// and the look-up structs table. To add more files, simply add them in the 'files' directory.
5
+
6
+// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
7
+
8
+
9
+// List all files
10
+$files = scandir(__DIR__.'/embed');
11
+
12
+$files = array_filter(array_map(function ($f) {
13
+  if (!is_file(__DIR__.'/embed/'.$f)) return null;
14
+  if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh$/', $f)) return null;
15
+
16
+  echo "Found: $f\n";
17
+  return $f;
18
+}, $files));
19
+
20
+sort($files);
21
+
22
+$formatted = array_filter(array_map(function ($f) {
23
+  return "\"files/embed/$f\"";
24
+}, $files));
25
+
26
+$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
27
+
28
+$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s', 
29
+  "#begin staticfiles\n".
30
+  "set(COMPONENT_EMBED_FILES\n        ".
31
+  implode("\n        ", $formatted) . ")\n".
32
+  "#end staticfiles", 
33
+  $cmake);
34
+
35
+file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
36
+
37
+
38
+// Generate a list of files
39
+
40
+$num = 0;
41
+$enum_keys = array_map(function ($f) use(&$num) {
42
+  $a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
43
+  return 'FILE_'. $a.' = '.($num++);
44
+}, $files);
45
+
46
+$keylist = implode(",\n    ", $enum_keys);
47
+
48
+$struct_array = [];
49
+
50
+$externs = array_map(function ($f) use (&$struct_array) {
51
+  $a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
52
+  
53
+  $start = '_binary_'. strtolower($a).'_start';
54
+  $end = '_binary_'. strtolower($a).'_end';
55
+  
56
+  static $mimes = array(
57
+    'txt' => 'text/plain',
58
+    'htm' => 'text/html',
59
+    'html' => 'text/html',
60
+    'php' => 'text/html',
61
+    'css' => 'text/css',
62
+    'js' => 'application/javascript',
63
+    'json' => 'application/json',
64
+    'xml' => 'application/xml',
65
+    'swf' => 'application/x-shockwave-flash',
66
+    'flv' => 'video/x-flv',
67
+    
68
+    'pem' => 'application/x-pem-file',
69
+
70
+    // images
71
+    'png' => 'image/png',
72
+    'jpe' => 'image/jpeg',
73
+    'jpeg' => 'image/jpeg',
74
+    'jpg' => 'image/jpeg',
75
+    'gif' => 'image/gif',
76
+    'bmp' => 'image/bmp',
77
+    'ico' => 'image/vnd.microsoft.icon',
78
+    'tiff' => 'image/tiff',
79
+    'tif' => 'image/tiff',
80
+    'svg' => 'image/svg+xml',
81
+    'svgz' => 'image/svg+xml',
82
+
83
+    // archives
84
+    'zip' => 'application/zip',
85
+    'rar' => 'application/x-rar-compressed',
86
+    'exe' => 'application/x-msdownload',
87
+    'msi' => 'application/x-msdownload',
88
+    'cab' => 'application/vnd.ms-cab-compressed',
89
+
90
+    // audio/video
91
+    'mp3' => 'audio/mpeg',
92
+    'qt' => 'video/quicktime',
93
+    'mov' => 'video/quicktime',
94
+
95
+    // adobe
96
+    'pdf' => 'application/pdf',
97
+    'psd' => 'image/vnd.adobe.photoshop',
98
+    'ai' => 'application/postscript',
99
+    'eps' => 'application/postscript',
100
+    'ps' => 'application/postscript',
101
+
102
+    // ms office
103
+    'doc' => 'application/msword',
104
+    'rtf' => 'application/rtf',
105
+    'xls' => 'application/vnd.ms-excel',
106
+    'ppt' => 'application/vnd.ms-powerpoint',
107
+
108
+    // open office
109
+    'odt' => 'application/vnd.oasis.opendocument.text',
110
+    'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
111
+  );
112
+  
113
+  $parts = explode('.', $f);
114
+  $suffix = end($parts);
115
+  $mime = $mimes[$suffix] ?? 'application/octet-stream';
116
+  
117
+  $len = filesize('embed/'.$f);
118
+  
119
+  $struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},";
120
+  
121
+  return 
122
+    'extern const uint8_t '.$start.'[];'."\n".
123
+    'extern const uint8_t '.$end.'[];';
124
+}, $files);
125
+
126
+$externlist = implode("\n", $externs);
127
+$structlist = implode("\n    ", $struct_array);
128
+
129
+
130
+file_put_contents('files_enum.h', <<<FILE
131
+// Do not change, auto-generated by gen_staticfiles.php
132
+
133
+#ifndef _EMBEDDED_FILES_ENUM_H
134
+#define _EMBEDDED_FILES_ENUM_H
135
+
136
+#include <stddef.h>
137
+#include <stdint.h>
138
+
139
+enum embedded_file_id {
140
+    $keylist,
141
+    FILE_MAX
142
+};
143
+
144
+struct embedded_file_info {
145
+  const uint8_t * start;
146
+  const uint8_t * end;
147
+  const char * name;
148
+  const char * mime;
149
+};
150
+
151
+$externlist
152
+
153
+extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[];
154
+
155
+#endif // _EMBEDDED_FILES_ENUM_H
156
+
157
+FILE
158
+);
159
+
160
+file_put_contents("files_enum.c", <<<FILE
161
+// Do not change, auto-generated by gen_staticfiles.php
162
+#include <stdint.h>
163
+#include "files_enum.h"
164
+
165
+const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
166
+    $structlist
167
+};
168
+
169
+FILE
170
+);

+ 22 - 0
components/fileserver/src/embedded_files.c View File

@@ -0,0 +1,22 @@
1
+#include <esp_err.h>
2
+#include "fileserver/embedded_files.h"
3
+#include "string.h"
4
+
5
+esp_err_t __attribute__((weak)) 
6
+www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file)
7
+{
8
+    // simple search by name
9
+    for(int i = 0; i < EMBEDDED_FILE_LOOKUP_LEN; i++) {
10
+        if (0 == strcmp(EMBEDDED_FILE_LOOKUP[i].name, name)) {
11
+            *file = &EMBEDDED_FILE_LOOKUP[i];
12
+            return ESP_OK;
13
+        }
14
+    }
15
+
16
+    return ESP_ERR_NOT_FOUND;
17
+}
18
+
19
+bool __attribute__((weak)) 
20
+www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access) {
21
+  return true;
22
+}

+ 619 - 0
components/fileserver/src/token_subs.c View File

@@ -0,0 +1,619 @@
1
+//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
2
+
3
+#include <esp_log.h>
4
+#include <esp_http_server.h>
5
+#include <rom/queue.h>
6
+#include <lwip/ip4_addr.h>
7
+#include <sys/param.h>
8
+#include <common_utils/utils.h>
9
+#include <fileserver/token_subs.h>
10
+
11
+#include "fileserver/embedded_files.h"
12
+#include "fileserver/token_subs.h"
13
+
14
+#define ESP_TRY(x) \
15
+    do {  \
16
+        esp_err_t try_er = (x);  \
17
+        if (try_er != ESP_OK) return try_er;  \
18
+    } while(0)
19
+
20
+static const char* TAG = "token_subs";
21
+
22
+// TODO implement buffering to avoid sending many tiny chunks when escaping
23
+
24
+/* encode for HTML. returns 0 or 1 - 1 = success */
25
+static esp_err_t send_html_chunk(httpd_req_t *r, const char *data, ssize_t len)
26
+{
27
+    assert(r);
28
+    assert(data);
29
+
30
+    int start = 0, end = 0;
31
+    char c;
32
+    if (len < 0) len = (int) strlen(data);
33
+    if (len==0) return ESP_OK;
34
+
35
+    for (end = 0; end < len; end++) {
36
+        c = data[end];
37
+        if (c == 0) {
38
+            // we found EOS
39
+            break; // not return - the last chunk is printed after the loop
40
+        }
41
+
42
+        if (c == '"' || c == '\'' || c == '<' || c == '>') {
43
+            if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
44
+            start = end + 1;
45
+        }
46
+
47
+        if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "&#34;", 5));
48
+        else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "&#39;", 5));
49
+        else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "&lt;", 4));
50
+        else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "&gt;", 4));
51
+    }
52
+
53
+    if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
54
+    return ESP_OK;
55
+}
56
+
57
+/* encode for JS. returns 0 or 1 - 1 = success */
58
+static esp_err_t send_js_chunk(httpd_req_t *r, const char *data, ssize_t len)
59
+{
60
+    assert(r);
61
+    assert(data);
62
+
63
+    int start = 0, end = 0;
64
+    char c;
65
+    if (len < 0) len = (int) strlen(data);
66
+    if (len==0) return ESP_OK;
67
+
68
+    for (end = 0; end < len; end++) {
69
+        c = data[end];
70
+        if (c == 0) {
71
+            // we found EOS
72
+            break; // not return - the last chunk is printed after the loop
73
+        }
74
+
75
+        if (c == '"' || c == '\\' || c == '/' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') {
76
+            if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
77
+            start = end + 1;
78
+        }
79
+
80
+        if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "\\\"", 2));
81
+        else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "\\'", 2));
82
+        else if (c == '\\') ESP_TRY(httpd_resp_send_chunk(r, "\\\\", 2));
83
+        else if (c == '/') ESP_TRY(httpd_resp_send_chunk(r, "\\/", 2));
84
+        else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "\\u003C", 6));
85
+        else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "\\u003E", 6));
86
+        else if (c == '\n') ESP_TRY(httpd_resp_send_chunk(r, "\\n", 2));
87
+        else if (c == '\r') ESP_TRY(httpd_resp_send_chunk(r, "\\r", 2));
88
+    }
89
+
90
+    if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start));
91
+    return ESP_OK;
92
+}
93
+
94
+
95
+esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape)
96
+{
97
+    switch (escape) {
98
+        default: // this enum should be exhaustive, but in case something went wrong, just print it verbatim
99
+
100
+        case TPL_ESCAPE_NONE:
101
+            return httpd_resp_send_chunk(r, buf, len);
102
+
103
+        case TPL_ESCAPE_HTML:
104
+            return send_html_chunk(r, buf, len);
105
+
106
+        case TPL_ESCAPE_JS:
107
+            return send_js_chunk(r, buf, len);
108
+    }
109
+}
110
+
111
+esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts)
112
+{
113
+    assert(file_index < EMBEDDED_FILE_LOOKUP_LEN);
114
+    const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index];
115
+
116
+    return httpd_send_static_file_struct(r, file, escape, opts);
117
+}
118
+
119
+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)
120
+{
121
+    if (0 == (opts & HTOPT_NO_HEADERS)) {
122
+        ESP_TRY(httpd_resp_set_type(r, file->mime));
123
+        ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "max-age=86400, public, must-revalidate"));
124
+    }
125
+
126
+    ESP_TRY(httpd_resp_send_chunk_escaped(r, (const char *) file->start, (size_t)(file->end - file->start), escape));
127
+
128
+    if (0 == (opts & HTOPT_NO_CLOSE)) {
129
+        ESP_TRY(httpd_resp_send_chunk(r, NULL, 0));
130
+    }
131
+
132
+    return ESP_OK;
133
+}
134
+
135
+esp_err_t httpd_send_template_file(httpd_req_t *r,
136
+                                   int file_index,
137
+                                   template_subst_t replacer,
138
+                                   void *context,
139
+                                   uint32_t opts)
140
+{
141
+    assert(file_index < EMBEDDED_FILE_LOOKUP_LEN);
142
+    const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index];
143
+    return httpd_send_template_file_struct(r,file,replacer,context,opts);
144
+}
145
+
146
+esp_err_t httpd_send_template_file_struct(httpd_req_t *r,
147
+                                   const struct embedded_file_info *file,
148
+                                   template_subst_t replacer,
149
+                                   void *context,
150
+                                   uint32_t opts)
151
+{
152
+    if (0 == (opts & HTOPT_NO_HEADERS)) {
153
+        ESP_TRY(httpd_resp_set_type(r, file->mime));
154
+        ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "no-cache, no-store, must-revalidate"));
155
+    }
156
+
157
+    return httpd_send_template(r, (const char *) file->start, (size_t)(file->end - file->start), replacer, context, opts);
158
+}
159
+
160
+esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape)
161
+{
162
+    assert(context);
163
+    assert(token);
164
+
165
+    struct tpl_kv_entry *entry;
166
+    struct tpl_kv_list *head = context;
167
+    SLIST_FOREACH(entry, head, link) {
168
+        if (0==strcmp(entry->key, token)) {
169
+            return httpd_resp_send_chunk_escaped(r,
170
+                entry->subst_heap ?
171
+                    entry->subst_heap :
172
+                    entry->subst,
173
+                -1, escape);
174
+        }
175
+    }
176
+
177
+    return ESP_ERR_NOT_FOUND;
178
+}
179
+
180
+struct stacked_replacer_context {
181
+    template_subst_t replacer0;
182
+    void *context0;
183
+    template_subst_t replacer1;
184
+    void *context1;
185
+};
186
+
187
+esp_err_t stacked_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape)
188
+{
189
+    assert(context);
190
+    assert(token);
191
+
192
+    struct stacked_replacer_context *combo = context;
193
+
194
+    if (ESP_OK == combo->replacer0(r, combo->context0, token, escape)) {
195
+        return ESP_OK;
196
+    }
197
+
198
+    if (ESP_OK == combo->replacer1(r, combo->context1, token, escape)) {
199
+        return ESP_OK;
200
+    }
201
+
202
+    return ESP_ERR_NOT_FOUND;
203
+}
204
+
205
+esp_err_t httpd_send_template(httpd_req_t *r,
206
+                              const char *template, ssize_t template_len,
207
+                              template_subst_t replacer,
208
+                              void *context,
209
+                              uint32_t opts)
210
+{
211
+    if (template_len < 0) template_len = strlen(template);
212
+
213
+    // replacer and context may be NULL
214
+    assert(template);
215
+    assert(r);
216
+
217
+    // data end
218
+    const char * const end = template + template_len;
219
+
220
+    // start of to-be-processed data
221
+    const char * pos = template;
222
+
223
+    // start position for finding opening braces, updated after a failed match to avoid infinite loop on the same bad token
224
+    const char * searchpos = pos;
225
+
226
+    // tokens must be copied to a buffer to allow adding the terminating null byte
227
+    char token_buf[MAX_TOKEN_LEN];
228
+
229
+    while (pos < end) {
230
+        const char * openbr = strchr(searchpos, '{');
231
+        if (openbr == NULL) {
232
+            // no more templates
233
+            ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos)));
234
+            break;
235
+        }
236
+
237
+        // this brace could start a valid template. check if it seems valid...
238
+        const char * closebr = strchr(openbr, '}');
239
+        if (closebr == NULL) {
240
+            // there are no further closing braces, so it can't be a template
241
+
242
+            // we also know there can't be any more substitutions, because they would lack a closing } too
243
+            ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos)));
244
+            break;
245
+        }
246
+
247
+        // see if the braces content looks like a token
248
+        const char *t = openbr + 1;
249
+        bool token_valid = true;
250
+        struct tpl_kv_list substitutions_head = tpl_kv_init();
251
+        struct tpl_kv_entry *new_subst_pair = NULL;
252
+
253
+        // a token can be either a name for replacement by the replacer func, or an include with static kv replacements
254
+        bool is_include = false;
255
+        bool token_is_optional = false;
256
+        const char *token_end = NULL; // points one char after the end of the token
257
+
258
+        // parsing the token
259
+        {
260
+            if (*t == '@') {
261
+                ESP_LOGD(TAG, "Parsing an Include token");
262
+                is_include = true;
263
+                t++;
264
+            }
265
+
266
+            enum {
267
+                P_NAME, P_KEY, P_VALUE
268
+            } state = P_NAME;
269
+
270
+            const char *kv_start = NULL;
271
+            while (t != closebr || state == P_VALUE) {
272
+                char c = *t;
273
+
274
+                if (state == P_NAME) {
275
+                    if (!((c >= 'a' && c <= 'z') ||
276
+                          (c >= 'A' && c <= 'Z') ||
277
+                          (c >= '0' && c <= '9') ||
278
+                          c == '.' || c == '_' || c == '-' || c == ':')) {
279
+
280
+                        if (!is_include && c == '?') {
281
+                            token_end = t;
282
+                            token_is_optional = true;
283
+                        } else {
284
+                            if (is_include && c == '|') {
285
+                                token_end = t;
286
+                                state = P_KEY;
287
+                                kv_start = t + 1;
288
+                                // pipe separates the include's filename and literal substitutions
289
+                                // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
290
+                            }
291
+                            else {
292
+                                token_valid = false;
293
+                                break;
294
+                            }
295
+                        }
296
+                    }
297
+                }
298
+                else if (state == P_KEY) {
299
+                    if (!((c >= 'a' && c <= 'z') ||
300
+                          (c >= 'A' && c <= 'Z') ||
301
+                          (c >= '0' && c <= '9') ||
302
+                          c == '.' || c == '_' || c == '-')) {
303
+                        if (c == '=') {
304
+                            new_subst_pair = calloc(1, sizeof(struct tpl_kv_entry));
305
+                            const size_t klen = MIN(TPL_KV_KEY_LEN, t - kv_start);
306
+                            strncpy(new_subst_pair->key, kv_start, klen);
307
+                            new_subst_pair->key[klen] = 0;
308
+
309
+                            kv_start = t + 1;
310
+
311
+                            state = P_VALUE;
312
+                            // pipe separates the include's filename and literal substitutions
313
+                            // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct
314
+                        }
315
+                    }
316
+                }
317
+                else if (state == P_VALUE) {
318
+                    if (c == '|' || c == '}') {
319
+                        const size_t vlen = MIN(TPL_KV_SUBST_LEN, t - kv_start);
320
+                        strncpy(new_subst_pair->subst, kv_start, vlen);
321
+                        new_subst_pair->subst[vlen] = 0;
322
+
323
+                        // attach the kv pair to the list
324
+                        SLIST_INSERT_HEAD(&substitutions_head, new_subst_pair, link);
325
+                        ESP_LOGD(TAG, "Adding subs kv %s -> %s", new_subst_pair->key, new_subst_pair->subst);
326
+                        new_subst_pair = NULL;
327
+
328
+                        kv_start = t + 1; // go past the pipe
329
+                        state = P_KEY;
330
+
331
+                        if (t == closebr) {
332
+                            break; // found the ending brace, so let's quit the kv parse loop
333
+                        }
334
+                    }
335
+                }
336
+
337
+                t++;
338
+            }
339
+            // clean up after a messed up subs kv pairs syntax
340
+            if (new_subst_pair != NULL) {
341
+                free(new_subst_pair);
342
+            }
343
+        }
344
+
345
+        if (!token_valid) {
346
+            // false match, include it in the block to send before the next token
347
+            searchpos = openbr + 1;
348
+            ESP_LOGD(TAG, "Skip invalid token near %10s", openbr);
349
+            continue;
350
+        }
351
+
352
+        // now we know it looks like a substitution token
353
+
354
+        // flush data before the token
355
+        if (pos != openbr) ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (openbr - pos)));
356
+
357
+        const char *token_start = openbr;
358
+
359
+        tpl_escape_t escape = TPL_ESCAPE_NONE;
360
+
361
+        // extract and terminate the token
362
+        size_t token_len = MIN(MAX_TOKEN_LEN-1, closebr - openbr - 1);
363
+        if (token_end) {
364
+            token_len = MIN(token_len, token_end - openbr - 1);
365
+        }
366
+
367
+        if (is_include) {
368
+            token_start += 1; // skip the @
369
+            token_len -= 1;
370
+        } else {
371
+            if (0 == strncmp("h:", openbr + 1, 2)) {
372
+                escape = TPL_ESCAPE_HTML;
373
+                token_start += 2;
374
+                token_len -= 2;
375
+            }
376
+            else if (0 == strncmp("j:", openbr + 1, 2)) {
377
+                escape = TPL_ESCAPE_JS;
378
+                token_start += 2;
379
+                token_len -= 2;
380
+            }
381
+        }
382
+
383
+        strncpy(token_buf, token_start+1, token_len);
384
+        token_buf[token_len] = 0;
385
+
386
+        ESP_LOGD(TAG, "Token: %s", token_buf);
387
+
388
+        esp_err_t rv;
389
+
390
+        if (is_include) {
391
+            ESP_LOGD(TAG, "Trying to include a sub-file");
392
+
393
+            const struct embedded_file_info *file = NULL;
394
+            rv = www_get_static_file(token_buf, FILE_ACCESS_PROTECTED, &file);
395
+            if (rv != ESP_OK) {
396
+                ESP_LOGE(TAG, "Failed to statically include \"%s\" in a template - %s", token_buf, esp_err_to_name(rv));
397
+                // this will cause the token to be emitted verbatim
398
+            } else {
399
+                ESP_LOGD(TAG, "Descending...");
400
+
401
+                // combine the two replacers
402
+                struct stacked_replacer_context combo = {
403
+                    .replacer0 = replacer,
404
+                    .context0 = context,
405
+                    .replacer1 = tpl_kv_replacer,
406
+                    .context1 = &substitutions_head
407
+                };
408
+
409
+                rv = httpd_send_template_file_struct(r, file, stacked_replacer, &combo, HTOPT_INCLUDE);
410
+                ESP_LOGD(TAG, "...back in parent");
411
+            }
412
+
413
+            // tear down the list
414
+            tpl_kv_free(&substitutions_head);
415
+
416
+            if (rv != ESP_OK) {
417
+                // just send it verbatim...
418
+                ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
419
+            }
420
+        } else {
421
+            if (replacer) {
422
+                ESP_LOGD(TAG, "Running replacer for \"%s\" with escape %d", token_buf, escape);
423
+                rv = replacer(r, context, token_buf, escape);
424
+
425
+                if (rv != ESP_OK) {
426
+                    if (rv == ESP_ERR_NOT_FOUND) {
427
+                        ESP_LOGD(TAG, "Token rejected");
428
+                        // optional token becomes empty string if not replaced
429
+                        if (!token_is_optional) {
430
+                            ESP_LOGD(TAG, "Not optional, keeping verbatim");
431
+                            // replacer rejected the token, keep it verbatim
432
+                            ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
433
+                        }
434
+                    }
435
+                    else {
436
+                        ESP_LOGE(TAG, "Unexpected error from replacer func: 0x%02x - %s", rv, esp_err_to_name(rv));
437
+                        return rv;
438
+                    }
439
+                }
440
+            } else {
441
+                ESP_LOGD(TAG, "Not replacer");
442
+                // no replacer, only includes - used for 'static' files
443
+                if (!token_is_optional) {
444
+                    ESP_LOGD(TAG, "Token not optional, keeping verbatim");
445
+                    ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1)));
446
+                }
447
+            }
448
+        }
449
+
450
+        searchpos = pos = closebr + 1;
451
+    }
452
+
453
+    if (0 == (opts & HTOPT_NO_CLOSE)) {
454
+        ESP_TRY(httpd_resp_send_chunk(r, NULL, 0));
455
+    }
456
+
457
+    return ESP_OK;
458
+}
459
+
460
+
461
+esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num)
462
+{
463
+    char buf[12];
464
+    itoa(num, buf, 10);
465
+    return tpl_kv_add(head, key, buf);
466
+}
467
+
468
+esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num)
469
+{
470
+    char buf[21];
471
+    sprintf(buf, "%"PRIi64, num);
472
+    return tpl_kv_add(head, key, buf);
473
+}
474
+
475
+esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h)
476
+{
477
+    char buf[IP4ADDR_STRLEN_MAX];
478
+    ip4_addr_t addr;
479
+    addr.addr = lwip_htonl(ip4h);
480
+    ip4addr_ntoa_r(&addr, buf, IP4ADDR_STRLEN_MAX);
481
+
482
+    return tpl_kv_add(head, key, buf);
483
+}
484
+
485
+esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst)
486
+{
487
+    ESP_LOGD(TAG, "kv add subs %s := %s", key, subst);
488
+    struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
489
+    if (entry == NULL) return ESP_ERR_NO_MEM;
490
+
491
+    assert(strlen(key) < TPL_KV_KEY_LEN);
492
+    assert(strlen(subst) < TPL_KV_SUBST_LEN);
493
+
494
+    strncpy(entry->key, key, TPL_KV_KEY_LEN);
495
+    entry->key[TPL_KV_KEY_LEN - 1] = 0;
496
+
497
+    strncpy(entry->subst, subst, TPL_KV_SUBST_LEN - 1);
498
+    entry->subst[TPL_KV_KEY_LEN - 1] = 0;
499
+
500
+    SLIST_INSERT_HEAD(head, entry, link);
501
+    return ESP_OK;
502
+}
503
+
504
+esp_err_t tpl_kv_add_heapstr(struct tpl_kv_list *head, const char *key, char *subst)
505
+{
506
+    ESP_LOGD(TAG, "kv add subs %s := (heap str)", key);
507
+    struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
508
+    if (entry == NULL) return ESP_ERR_NO_MEM;
509
+
510
+    assert(strlen(key) < TPL_KV_KEY_LEN);
511
+
512
+    strncpy(entry->key, key, TPL_KV_KEY_LEN);
513
+    entry->key[TPL_KV_KEY_LEN - 1] = 0;
514
+
515
+    entry->subst_heap = subst;
516
+
517
+    SLIST_INSERT_HEAD(head, entry, link);
518
+    return ESP_OK;
519
+}
520
+
521
+esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...)
522
+{
523
+    ESP_LOGD(TAG, "kv printf %s := %s", key, format);
524
+    struct tpl_kv_entry *entry = calloc(1, sizeof(struct tpl_kv_entry));
525
+    if (entry == NULL) return ESP_ERR_NO_MEM;
526
+
527
+    assert(strlen(key) < TPL_KV_KEY_LEN);
528
+
529
+    strncpy(entry->key, key, TPL_KV_KEY_LEN);
530
+    entry->key[TPL_KV_KEY_LEN - 1] = 0;
531
+
532
+    va_list list;
533
+    va_start(list, format);
534
+    vsnprintf(entry->subst, TPL_KV_SUBST_LEN, format, list);
535
+    va_end(list);
536
+    entry->subst[TPL_KV_KEY_LEN - 1] = 0;
537
+
538
+    SLIST_INSERT_HEAD(head, entry, link);
539
+    return ESP_OK;
540
+}
541
+
542
+void tpl_kv_free(struct tpl_kv_list *head)
543
+{
544
+    struct tpl_kv_entry *item, *next;
545
+    SLIST_FOREACH_SAFE(item, head, link, next) {
546
+        if (item->subst_heap) {
547
+            free(item->subst_heap);
548
+            item->subst_heap = NULL;
549
+        }
550
+
551
+        free(item);
552
+    }
553
+}
554
+
555
+esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head)
556
+{
557
+    httpd_resp_set_type(req, "text/plain; charset=utf-8");
558
+
559
+#define BUF_CAP 512
560
+    char *buf_head = malloc(BUF_CAP);
561
+    if (!buf_head) {
562
+        ESP_LOGE(TAG, "Malloc err");
563
+        return ESP_FAIL;
564
+    }
565
+    char *buf = buf_head;
566
+    size_t cap = BUF_CAP;
567
+    struct tpl_kv_entry *entry;
568
+
569
+    // GCC nested function
570
+    esp_err_t send_part() {
571
+        esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap);
572
+        buf = buf_head; // buf is assigned to buf head
573
+        cap = BUF_CAP;
574
+        if (suc != ESP_OK) {
575
+            ESP_LOGE(TAG, "Error sending buffer");
576
+            free(buf_head);
577
+            httpd_resp_send_chunk(req, NULL, 0);
578
+        }
579
+        return suc;
580
+    }
581
+
582
+    SLIST_FOREACH(entry, head, link) {
583
+        while(NULL == (buf = append(buf, entry->key, &cap))) ESP_TRY(send_part());
584
+        while(NULL == (buf = append(buf, "\x1f", &cap))) ESP_TRY(send_part());
585
+
586
+        if (entry->subst_heap) {
587
+            if (strlen(entry->subst_heap) >= BUF_CAP) {
588
+                // send what we have
589
+                ESP_TRY(send_part());
590
+                esp_err_t suc = httpd_resp_send_chunk(req, entry->subst_heap, -1);
591
+                if (suc != ESP_OK) {
592
+                    ESP_LOGE(TAG, "Error sending buffer");
593
+                    free(buf_head);
594
+                    httpd_resp_send_chunk(req, NULL, 0);
595
+                }
596
+            } else {
597
+                while (NULL == (buf = append(buf, entry->subst_heap, &cap))) ESP_TRY(send_part());
598
+            }
599
+        } else {
600
+            while(NULL == (buf = append(buf, entry->subst, &cap))) ESP_TRY(send_part());
601
+        }
602
+
603
+        if (entry->link.sle_next) {
604
+            while(NULL == (buf = append(buf, "\x1e", &cap))) ESP_TRY(send_part());
605
+        }
606
+    }
607
+    // send leftovers
608
+    if (buf != buf_head) {
609
+        esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap);
610
+        if (suc != ESP_OK) {
611
+            ESP_LOGE(TAG, "Error sending buffer");
612
+        }
613
+    }
614
+
615
+    // Commit
616
+    httpd_resp_send_chunk(req, NULL, 0);
617
+    free(buf_head);
618
+    return ESP_OK;
619
+}

+ 9 - 0
components/httpd_utils/CMakeLists.txt View File

@@ -0,0 +1,9 @@
1
+set(COMPONENT_ADD_INCLUDEDIRS
2
+        "include")
3
+
4
+set(COMPONENT_SRCDIRS
5
+        "src")
6
+
7
+set(COMPONENT_REQUIRES tcpip_adapter esp_http_server common_utils)
8
+
9
+register_component()

+ 4 - 0
components/httpd_utils/README.txt View File

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

+ 3 - 0
components/httpd_utils/component.mk View File

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

+ 32 - 0
components/httpd_utils/include/httpd_utils/captive.h View File

@@ -0,0 +1,32 @@
1
+#ifndef HTTPD_UTILS_CAPTIVE_H
2
+#define HTTPD_UTILS_CAPTIVE_H
3
+
4
+#include <esp_http_server.h>
5
+#include <esp_err.h>
6
+
7
+/**
8
+ * Redirect if needed when a captive portal capture is detected
9
+ *
10
+ * @param r
11
+ * @return ESP_OK on redirect, ESP_ERR_NOT_FOUND if not needed, other error on failure
12
+ */
13
+esp_err_t httpd_captive_redirect(httpd_req_t *r);
14
+
15
+/**
16
+ * Get URL to redirect to. WEAK.
17
+ *
18
+ * @param r
19
+ * @param buf
20
+ * @param maxlen
21
+ * @return http(s)://foo.bar/
22
+ */
23
+esp_err_t httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen);
24
+
25
+/**
26
+ * Get captive portal domain. WEAK.
27
+ *
28
+ * @return foo.bar
29
+ */
30
+const char * httpd_captive_redirect_get_domain();
31
+
32
+#endif //HTTPD_UTILS_CAPTIVE_H

+ 13 - 0
components/httpd_utils/include/httpd_utils/fd_to_ipv4.h View File

@@ -0,0 +1,13 @@
1
+#ifndef HTTPD_FDIPV4_H
2
+#define HTTPD_FDIPV4_H
3
+
4
+/**
5
+ * Get IP address for a FD
6
+ *
7
+ * @param fd
8
+ * @param[out] ipv4
9
+ * @return success
10
+ */
11
+esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4);
12
+
13
+#endif //HTTPD_FDIPV4_H

+ 16 - 0
components/httpd_utils/include/httpd_utils/redirect.h View File

@@ -0,0 +1,16 @@
1
+#ifndef HTTPD_UTILS_REDIRECT_H
2
+#define HTTPD_UTILS_REDIRECT_H
3
+
4
+#include <esp_http_server.h>
5
+#include <esp_err.h>
6
+
7
+/**
8
+ * Redirect to other URI - sends a HTTP response from the http server
9
+ *
10
+ * @param r - request
11
+ * @param uri - target uri
12
+ * @return success
13
+ */
14
+esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri);
15
+
16
+#endif // HTTPD_UTILS_REDIRECT_H

+ 55 - 0
components/httpd_utils/include/httpd_utils/session.h View File

@@ -0,0 +1,55 @@
1
+/**
2
+ * Session store system main include file
3
+ *
4
+ * Created on 2019/07/13.
5
+ */
6
+
7
+#ifndef HTTPD_UTILS_SESSION_H
8
+#define HTTPD_UTILS_SESSION_H
9
+
10
+#include "session_kvmap.h"
11
+#include "session_store.h"
12
+
13
+// Customary keys
14
+
15
+/** Session key for OK flash message */
16
+#define SESS_FLASH_OK "flash_ok"
17
+/** Session key for error flash message */
18
+#define SESS_FLASH_ERR "flash_err"
19
+/** Session key for a "logged in" flag. Value is 1 if logged in. */
20
+#define SESS_AUTHED "authed"
21
+
22
+// ..
23
+
24
+/**
25
+ * Redirect to /login form if unauthed,
26
+ * but also retrieve the session key-value store for further use
27
+ */
28
+#define HTTP_GET_AUTHED_SESSION(kvstore, r) do {                            \
29
+    kvstore = httpd_req_find_session_and((r), SESS_GET_DATA);               \
30
+    if (NULL == kvstore || NULL == sess_kv_map_get(kvstore, SESS_AUTHED)) { \
31
+        return httpd_redirect_to((r), "/login");                            \
32
+    }                                                                       \
33
+} while(0)
34
+
35
+/**
36
+ * Start or resume a session without checking for authed status.
37
+ * When started, the session cookie is added to the response immediately.
38
+ *
39
+ * @param[out] kvstore - a place to store the allocated or retrieved session kvmap
40
+ * @param[in] r - request
41
+ * @return success
42
+ */
43
+esp_err_t HTTP_GET_SESSION(sess_kv_map_t **kvstore, httpd_req_t *r);
44
+
45
+/**
46
+ * Redirect to the login form if unauthed.
47
+ * This is the same as `HTTP_GET_AUTHED_SESSION`, except the kvstore variable is
48
+ * not needed in the uri handler calling this, so it is declared internally.
49
+ */
50
+#define HTTP_REDIRECT_IF_UNAUTHED(r) do {                             \
51
+    sess_kv_map_t *_kvstore;                                          \
52
+    HTTP_GET_AUTHED_SESSION(_kvstore, r);                             \
53
+} while(0)
54
+
55
+#endif // HTTPD_UTILS_SESSION_H

+ 77 - 0
components/httpd_utils/include/httpd_utils/session_kvmap.h View File

@@ -0,0 +1,77 @@
1
+/**
2
+ * Simple key-value map for session data storage.
3
+ * Takes care of dynamic allocation and cleanup.
4
+ * 
5
+ * Created on 2019/01/28.
6
+ */
7
+
8
+#ifndef SESSION_KVMAP_H
9
+#define SESSION_KVMAP_H
10
+
11
+/**
12
+ * Prototype for a free() func to clean up session-held objects
13
+ */
14
+typedef void (*sess_kv_free_func_t)(void *obj);
15
+
16
+typedef struct sess_kv_map sess_kv_map_t;
17
+
18
+#define SESS_KVMAP_KEY_LEN 16
19
+
20
+/**
21
+ * Allocate a new session key-value store
22
+ *
23
+ * @return the store, NULL on error
24
+ */
25
+sess_kv_map_t *sess_kv_map_alloc(void);
26
+
27
+/**
28
+ * Free the session kv store.
29
+ *
30
+ * @param head - store head
31
+ */
32
+void sess_kv_map_free(void *head);
33
+
34
+/**
35
+ * Get a value from the session kv store.
36
+ *
37
+ * @param head - store head
38
+ * @param key - key to get a value for
39
+ * @return the value, or NULL if not found
40
+ */
41
+void *sess_kv_map_get(sess_kv_map_t *head, const char *key);
42
+
43
+/**
44
+ * Get and remove a value from the session store.
45
+ *
46
+ * The free function is not called in this case and the recipient is
47
+ * responsible for cleaning it up correctly.
48
+ *
49
+ * @param head - store head
50
+ * @param key - key to get a value for
51
+ * @return the value, or NULL if not found
52
+ */
53
+void * sess_kv_map_take(sess_kv_map_t *head, const char *key);
54
+
55
+/**
56
+ * Remove an entry from the session by its key name.
57
+ * The slot is not free'd yet, but is made available for reuse.
58
+ *
59
+ * @param head - store head
60
+ * @param key - key to remove
61
+ * @return success
62
+ */
63
+esp_err_t sess_kv_map_remove(sess_kv_map_t *head, const char *key);
64
+
65
+/**
66
+ * 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.
67
+ * Otherwise a new slot is allocated for it, or a previously released one is reused.
68
+ *
69
+ * @param head - store head
70
+ * @param key - key to assign to
71
+ * @param value - new value
72
+ * @param free_fn - value free func
73
+ * @return success
74
+ */
75
+esp_err_t sess_kv_map_set(sess_kv_map_t *head, const char *key, void *value, sess_kv_free_func_t free_fn);
76
+
77
+#endif //SESSION_KVMAP_H

+ 100 - 0
components/httpd_utils/include/httpd_utils/session_store.h View File

@@ -0,0 +1,100 @@
1
+/**
2
+ * Cookie-based session store
3
+ */
4
+
5
+#ifndef SESSION_STORE_H
6
+#define SESSION_STORE_H
7
+
8
+#include "esp_http_server.h"
9
+
10
+#define SESSION_EXPIRY_TIME_S 60*30
11
+#define SESSION_COOKIE_NAME "SESSID"
12
+
13
+/** function that frees a session data object */
14
+typedef void (*sess_data_free_fn_t)(void *);
15
+
16
+enum session_find_action {
17
+    SESS_DROP, SESS_GET_DATA
18
+};
19
+
20
+/**
21
+ * Find session and either get data, or drop it.
22
+ *
23
+ * @param cookie
24
+ * @param action
25
+ * @return
26
+ */
27
+void *session_find_and(const char *cookie, enum session_find_action action);
28
+
29
+/**
30
+ * Initialize the session store.
31
+ * Safely empty it if initialized
32
+ */
33
+void session_store_init(void);
34
+
35
+// placeholder for when no data is stored
36
+#define SESSION_DUMMY ((void *) 1)
37
+
38
+/**
39
+ * Create a new session. Data must not be NULL, because it wouldn't be possible
40
+ * to distinguish between NULL value and session not found in return values.
41
+ * It can be e.g. 1 if no data storage is needed.
42
+ *
43
+ * @param data - data object to attach to the session
44
+ * @param free_fn - function that disposes of the data when the session expires
45
+ * @return NULL on error, or the new session ID. This is a live pointer into the session structure,
46
+ *         must be copied if stored, as it can become invalid at any time
47
+ */
48
+const char *session_new(void *data, sess_data_free_fn_t free_fn);
49
+
50
+/**
51
+ * Find a session by it's ID (from a cookie)
52
+ *
53
+ * @param cookie - session ID string
54
+ * @return session data (void*), or NULL
55
+ */
56
+void *session_find(const char *cookie);
57
+
58
+/**
59
+ * Loop through all sessions and drop these that expired.
60
+ */
61
+void session_drop_expired(void);
62
+
63
+/**
64
+ * Drop a session by its ID. Does nothing if not found.
65
+ *
66
+ * @param cookie - session ID string
67
+ */
68
+void session_drop(const char *cookie);
69
+
70
+/**
71
+ * Parse the Cookie header from a request, and do something with the corresponding session.
72
+ *
73
+ * To also delete the cookie, use req_delete_session_cookie(r)
74
+ *
75
+ * @param r - request
76
+ * @param action - what to do with the session
77
+ * @return session data, NULL if removed or not found
78
+ */
79
+void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action);
80
+
81
+/**
82
+ * Add a header that deletes the session cookie
83
+ *
84
+ * @param r - request
85
+ */
86
+void httpd_resp_delete_session_cookie(httpd_req_t *r);
87
+
88
+/**
89
+ * Add a header that sets the session cookie.
90
+ *
91
+ * This must be called after creating a session (e.g. user logged in) to make it persistent.
92
+ *
93
+ * @attention NOT RE-ENTRANT, CAN'T BE USED AGAIN UNTIL THE REQUEST IT WAS CALLED FOR IS DISPATCHED.
94
+ *
95
+ * @param r - request
96
+ * @param cookie - cookie ID
97
+ */
98
+void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie);
99
+
100
+#endif //SESSION_STORE_H

+ 106 - 0
components/httpd_utils/src/captive.c View File

@@ -0,0 +1,106 @@
1
+#include <esp_wifi_types.h>
2
+#include <esp_wifi.h>
3
+#include <esp_log.h>
4
+#include <fcntl.h>
5
+#include <sys/socket.h>
6
+#include <sys/param.h>
7
+
8
+#include "httpd_utils/captive.h"
9
+#include "httpd_utils/fd_to_ipv4.h"
10
+#include "httpd_utils/redirect.h"
11
+#include <common_utils/utils.h>
12
+
13
+static const char *TAG="captive";
14
+
15
+const char * __attribute__((weak))
16
+httpd_captive_redirect_get_domain(void)
17
+{
18
+    return "fb_node.captive";
19
+}
20
+
21
+esp_err_t __attribute__((weak))
22
+httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen)
23
+{
24
+    buf = append(buf, "http://", &maxlen);
25
+    buf = append(buf, httpd_captive_redirect_get_domain(), &maxlen);
26
+    append(buf, "/", &maxlen);
27
+
28
+    return ESP_OK;
29
+}
30
+
31
+esp_err_t httpd_captive_redirect(httpd_req_t *r)
32
+{
33
+    // must be static to survive being used in the redirect header
34
+    static char s_buf[64];
35
+
36
+    wifi_mode_t mode = 0;
37
+    esp_wifi_get_mode(&mode);
38
+
39
+    // Check if we have an softap interface. No point checking IPs and hostnames if the client can't be on AP.
40
+    if (mode == WIFI_MODE_STA || mode == WIFI_MODE_NULL) {
41
+        goto no_redirect;
42
+    }
43
+
44
+    int fd = httpd_req_to_sockfd(r);
45
+
46
+    tcpip_adapter_ip_info_t apip;
47
+    tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &apip);
48
+
49
+    u32_t client_addr;
50
+    if(ESP_OK != fd_to_ipv4(fd, &client_addr)) {
51
+        return ESP_FAIL;
52
+    }
53
+
54
+    ESP_LOGD(TAG, "[captive] Client addr = 0x%08x, ap addr 0x%08x, ap nmask 0x%08x",
55
+             client_addr,
56
+             apip.ip.addr,
57
+             apip.netmask.addr
58
+    );
59
+
60
+    // Check if client IP looks like from our AP dhcps
61
+    if ((client_addr & apip.netmask.addr) != (apip.ip.addr & apip.netmask.addr)) {
62
+        ESP_LOGD(TAG, "[captive] Client not in AP IP range");
63
+        goto no_redirect;
64
+    }
65
+
66
+    // Get requested hostname from the header
67
+    esp_err_t rv = httpd_req_get_hdr_value_str(r, "Host", s_buf, 64);
68
+    if (rv != ESP_OK) {
69
+        ESP_LOGW(TAG, "[captive] No host in request?");
70
+        goto no_redirect;
71
+    }
72
+
73
+    ESP_LOGD(TAG, "[captive] Candidate for redirect: %s%s", s_buf, r->uri);
74
+
75
+    // Never redirect if host is an IP
76
+    if (strlen(s_buf)>7) {
77
+        bool isIP = 1;
78
+        for (int x = 0; x < strlen(s_buf); x++) {
79
+            if (s_buf[x] != '.' && (s_buf[x] < '0' || s_buf[x] > '9')) {
80
+                isIP = 0;
81
+                break;
82
+            }
83
+        }
84
+
85
+        if (isIP) {
86
+            ESP_LOGD(TAG, "[captive] Access via IP, no redirect needed");
87
+            goto no_redirect;
88
+        }
89
+    }
90
+
91
+    // Redirect if host differs
92
+    // - this can be e.g. connectivitycheck.gstatic.com or the equivalent for ios
93
+
94
+    if (0 != strcmp(s_buf, httpd_captive_redirect_get_domain())) {
95
+        ESP_LOGD(TAG, "[captive] Host differs, redirecting...");
96
+
97
+        httpd_captive_redirect_get_url(r, s_buf, 64);
98
+        return httpd_redirect_to(r, s_buf);
99
+    } else {
100
+        ESP_LOGD(TAG, "[captive] Host is OK");
101
+        goto no_redirect;
102
+    }
103
+
104
+    no_redirect:
105
+    return ESP_ERR_NOT_FOUND;
106
+}

+ 42 - 0
components/httpd_utils/src/fd_to_ipv4.c View File

@@ -0,0 +1,42 @@
1
+#include <esp_wifi_types.h>
2
+#include <esp_wifi.h>
3
+#include <esp_log.h>
4
+#include <fcntl.h>
5
+#include <sys/socket.h>
6
+
7
+#include "httpd_utils/fd_to_ipv4.h"
8
+
9
+static const char *TAG = "fd2ipv4";
10
+
11
+/**
12
+ * Get IP address for a FD
13
+ *
14
+ * @param fd
15
+ * @param[out] ipv4
16
+ * @return success
17
+ */
18
+esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4)
19
+{
20
+    struct sockaddr_in6 addr;
21
+    size_t len = sizeof(addr);
22
+    int rv = getpeername(fd, (struct sockaddr *) &addr, &len);
23
+    if (rv != 0) {
24
+        ESP_LOGE(TAG, "Failed to get IP addr for fd %d", fd);
25
+        return ESP_FAIL;
26
+    }
27
+
28
+    uint32_t client_addr = 0;
29
+    if (addr.sin6_family == AF_INET6) {
30
+        // this would fail in a real ipv6 network
31
+        // with ipv4 the addr is simply in the last ipv6 byte
32
+        struct sockaddr_in6 *s = &addr;
33
+        client_addr = s->sin6_addr.un.u32_addr[3];
34
+    }
35
+    else {
36
+        struct sockaddr_in *s = (struct sockaddr_in *) &addr;
37
+        client_addr = s->sin_addr.s_addr;
38
+    }
39
+
40
+    *ipv4 = client_addr;
41
+    return ESP_OK;
42
+}

+ 20 - 0
components/httpd_utils/src/redirect.c View File

@@ -0,0 +1,20 @@
1
+#include <esp_wifi_types.h>
2
+#include <esp_wifi.h>
3
+#include <esp_log.h>
4
+#include <fcntl.h>
5
+#include <sys/socket.h>
6
+
7
+#include "httpd_utils/redirect.h"
8
+
9
+static const char *TAG="redirect";
10
+
11
+esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri)
12
+{
13
+    ESP_LOGD(TAG, "Redirect to %s", uri);
14
+
15
+    httpd_resp_set_hdr(r, "Location", uri);
16
+    httpd_resp_set_status(r, "303 See Other");
17
+    httpd_resp_set_type(r, HTTPD_TYPE_TEXT);
18
+    const char *msg = "Redirect";
19
+    return httpd_resp_send(r, msg, -1);
20
+}

+ 181 - 0
components/httpd_utils/src/session_kvmap.c View File

@@ -0,0 +1,181 @@
1
+//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
2
+
3
+#include <rom/queue.h>
4
+#include <malloc.h>
5
+#include <assert.h>
6
+#include <esp_log.h>
7
+#include <esp_err.h>
8
+#include <string.h>
9
+#include "httpd_utils/session_kvmap.h"
10
+
11
+static const char *TAG = "sess_kvmap";
12
+
13
+// this struct is opaque, a stub like this is sufficient for the head pointer.
14
+struct sess_kv_entry;
15
+
16
+/** Session head structure, dynamically allocated */
17
+SLIST_HEAD(sess_kv_map, sess_kv_entry);
18
+
19
+struct sess_kv_entry {
20
+    SLIST_ENTRY(sess_kv_entry) link;
21
+    char key[SESS_KVMAP_KEY_LEN];
22
+    void *value;
23
+    sess_kv_free_func_t free_fn;
24
+};
25
+
26
+struct sess_kv_map *sess_kv_map_alloc(void)
27
+{
28
+    ESP_LOGD(TAG, "kv store alloc");
29
+    struct sess_kv_map *map = malloc(sizeof(struct sess_kv_map));
30
+    assert(map);
31
+    SLIST_INIT(map);
32
+    return map;
33
+}
34
+
35
+void sess_kv_map_free(void *head_v)
36
+{
37
+    struct sess_kv_map* head = head_v;
38
+
39
+    ESP_LOGD(TAG, "kv store free");
40
+    assert(head);
41
+    struct sess_kv_entry *item, *tmp;
42
+    SLIST_FOREACH_SAFE(item, head, link, tmp) {
43
+        if (item->free_fn) {
44
+            item->free_fn(item->value);
45
+            free(item);
46
+        }
47
+    }
48
+    free(head);
49
+}
50
+
51
+
52
+void * sess_kv_map_get(struct sess_kv_map *head, const char *key)
53
+{
54
+    assert(head);
55
+    assert(key);
56
+    ESP_LOGD(TAG, "kv store get %s", key);
57
+
58
+    struct sess_kv_entry *item;
59
+    SLIST_FOREACH(item, head, link) {
60
+        if (0==strcmp(item->key, key)) {
61
+            ESP_LOGD(TAG, "got ok");
62
+            return item->value;
63
+        }
64
+    }
65
+
66
+    ESP_LOGD(TAG, "not found in store");
67
+    return NULL;
68
+}
69
+
70
+void * sess_kv_map_take(struct sess_kv_map *head, const char *key)
71
+{
72
+    assert(head);
73
+    assert(key);
74
+    ESP_LOGD(TAG, "kv store take %s", key);
75
+
76
+    struct sess_kv_entry *item;
77
+    SLIST_FOREACH(item, head, link) {
78
+        if (0==strcmp(item->key, key)) {
79
+            item->key[0] = 0;
80
+            item->free_fn = NULL;
81
+            ESP_LOGD(TAG, "taken ok");
82
+            return item->value;
83
+        }
84
+    }
85
+
86
+    ESP_LOGD(TAG, "not found in store");
87
+    return NULL;
88
+}
89
+
90
+esp_err_t sess_kv_map_remove(struct sess_kv_map *head, const char *key)
91
+{
92
+    assert(head);
93
+    assert(key);
94
+    ESP_LOGD(TAG, "kv store remove %s", key);
95
+
96
+    struct sess_kv_entry *item;
97
+    SLIST_FOREACH(item, head, link) {
98
+        if (0==strcmp(item->key, key)) {
99
+            if (item->free_fn) {
100
+                item->free_fn(item->value);
101
+            }
102
+            item->key[0] = 0;
103
+            item->value = NULL;
104
+            item->free_fn = NULL;
105
+            return ESP_OK;
106
+        }
107
+    }
108
+
109
+    ESP_LOGD(TAG, "couldn't remove, not found: %s", key);
110
+    return ESP_ERR_NOT_FOUND;
111
+}
112
+
113
+
114
+esp_err_t sess_kv_map_set(struct sess_kv_map *head, const char *key, void *value, sess_kv_free_func_t free_fn)
115
+{
116
+    assert(head);
117
+    assert(key);
118
+    ESP_LOGD(TAG, "kv set value for key %s", key);
119
+
120
+    size_t key_len = strlen(key);
121
+    if (key_len > SESS_KVMAP_KEY_LEN-1) {
122
+        ESP_LOGE(TAG, "Key too long: %s", key);
123
+        // discard illegal key
124
+        return ESP_FAIL;
125
+    }
126
+
127
+    if (key_len == 0) {
128
+        ESP_LOGE(TAG, "Key too short: \"%s\"", key);
129
+        // discard illegal key
130
+        return ESP_FAIL;
131
+    }
132
+
133
+    struct sess_kv_entry *item = NULL;
134
+    struct sess_kv_entry *empty_item = NULL; // found item with no content
135
+    SLIST_FOREACH(item, head, link) {
136
+        ESP_LOGD(TAG, "test item with key %s, ptr %p > %p", item->key, item, item->link.sle_next);
137
+        if (0 == item->key[0]) {
138
+            ESP_LOGD(TAG, "found an empty slot");
139
+            empty_item = item;
140
+        }
141
+        else if (0==strcmp(item->key, key)) {
142
+            ESP_LOGD(TAG, "old value replaced");
143
+            if (item->free_fn) {
144
+                item->free_fn(item->value);
145
+            }
146
+            item->value = value;
147
+            item->free_fn = free_fn;
148
+            return ESP_OK;
149
+        } else {
150
+            ESP_LOGD(TAG, "skip this one");
151
+        }
152
+    }
153
+
154
+    struct sess_kv_entry *new_item = NULL;
155
+
156
+    // insert new or reuse an empty item
157
+    if (empty_item) {
158
+        new_item = empty_item;
159
+        ESP_LOGD(TAG, "empty item reused (%p)", new_item);
160
+    } else {
161
+        ESP_LOGD(TAG, "alloc new item");
162
+        // key not found, add a new entry.
163
+        new_item = malloc(sizeof(struct sess_kv_entry));
164
+        if (!new_item) {
165
+            ESP_LOGE(TAG, "New entry alloc failed");
166
+            return ESP_ERR_NO_MEM;
167
+        }
168
+    }
169
+
170
+    strcpy(new_item->key, key);
171
+    new_item->free_fn = free_fn;
172
+    new_item->value = value;
173
+
174
+    if (!empty_item) {
175
+        ESP_LOGD(TAG, "insert new item into list");
176
+        // this item was malloc'd
177
+        SLIST_INSERT_HEAD(head, new_item, link);
178
+    }
179
+
180
+    return ESP_OK;
181
+}

+ 220 - 0
components/httpd_utils/src/session_store.c View File

@@ -0,0 +1,220 @@
1
+//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
2
+
3
+#include <malloc.h>
4
+#include <esp_log.h>
5
+#include <freertos/FreeRTOS.h>
6
+#include <freertos/semphr.h>
7
+#include <esp_http_server.h>
8
+#include <rom/queue.h>
9
+
10
+#include "httpd_utils/session_store.h"
11
+
12
+// TODO add a limit on simultaneously open sessions (can cause memory exhaustion DoS)
13
+
14
+#define COOKIE_LEN 32
15
+static const char *TAG = "session";
16
+
17
+struct session {
18
+    char cookie[COOKIE_LEN + 1];
19
+    void *data;
20
+    TickType_t last_activity_time;
21
+    LIST_ENTRY(session) link;
22
+    sess_data_free_fn_t free_fn;
23
+};
24
+
25
+static LIST_HEAD(sessions_, session) s_store;
26
+
27
+static SemaphoreHandle_t sess_store_lock = NULL;
28
+static bool sess_store_inited = false;
29
+
30
+
31
+void session_store_init(void)
32
+{
33
+    if (sess_store_inited) {
34
+        xSemaphoreTake(sess_store_lock, portMAX_DELAY);
35
+        {
36
+            struct session *it, *tit;
37
+            LIST_FOREACH_SAFE(it, &s_store, link, tit) {
38
+                ESP_LOGW(TAG, "Session cookie expired: \"%s\"", it->cookie);
39
+                if (it->free_fn) it->free_fn(it->data);
40
+                // no relink, we dont care if the list breaks after this - we're removing all of it
41
+                free(it);
42
+            }
43
+        }
44
+        LIST_INIT(&s_store);
45
+        xSemaphoreGive(sess_store_lock);
46
+    } else {
47
+        LIST_INIT(&s_store);
48
+        sess_store_lock = xSemaphoreCreateMutex();
49
+        sess_store_inited = true;
50
+    }
51
+}
52
+
53
+/**
54
+ * Fill buffer with base64 symbols. Does not add a trailing null byte
55
+ *
56
+ * @param buf
57
+ * @param len
58
+ */
59
+static void esp_fill_random_alnum(char *buf, size_t len)
60
+{
61
+#define alphabet_len 64
62
+    static const char alphabet[alphabet_len] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
63
+
64
+    unsigned int seed = xTaskGetTickCount();
65
+
66
+    assert(buf != NULL);
67
+    for (int i = 0; i < len; i++) {
68
+        int index = rand_r(&seed) % alphabet_len;
69
+        *buf++ = (uint8_t) alphabet[index];
70
+    }
71
+}
72
+
73
+const char *session_new(void *data, sess_data_free_fn_t free_fn)
74
+{
75
+    assert(data != NULL);
76
+
77
+    struct session *item = malloc(sizeof(struct session));
78
+    if (item == NULL) return NULL;
79
+
80
+    item->data = data;
81
+    item->free_fn = free_fn;
82
+    esp_fill_random_alnum(item->cookie, COOKIE_LEN);
83
+    item->cookie[COOKIE_LEN] = 0; // add the terminator
84
+
85
+    xSemaphoreTake(sess_store_lock, portMAX_DELAY);
86
+    {
87
+        item->last_activity_time = xTaskGetTickCount();
88
+
89
+        LIST_INSERT_HEAD(&s_store, item, link);
90
+    }
91
+    xSemaphoreGive(sess_store_lock);
92
+
93
+    ESP_LOGD(TAG, "New HTTP session: %s", item->cookie);
94
+
95
+    return item->cookie;
96
+}
97
+
98
+void *session_find_and(const char *cookie, enum session_find_action action)
99
+{
100
+    // no point in searching if the length is wrong
101
+    if (strlen(cookie) != COOKIE_LEN) {
102
+        ESP_LOGW(TAG, "Wrong session cookie length: \"%s\"", cookie);
103
+        return NULL;
104
+    }
105
+
106
+    struct session *it = NULL;
107
+
108
+    bool found = false;
109
+    xSemaphoreTake(sess_store_lock, portMAX_DELAY);
110
+    {
111
+        LIST_FOREACH(it, &s_store, link) {
112
+            if (0==strcmp(it->cookie, cookie)) {
113
+                ESP_LOGD(TAG, "Session cookie matched: \"%s\"", cookie);
114
+
115
+                it->last_activity_time = xTaskGetTickCount();
116
+                found = true;
117
+                break;
118
+            }
119
+        }
120
+        if (found && action == SESS_DROP) {
121
+            if (it->free_fn) it->free_fn(it->data);
122
+            LIST_REMOVE(it, link);
123
+            free(it);
124
+            ESP_LOGD(TAG, "Dropped session: \"%s\"", cookie);
125
+        }
126
+    }
127
+    xSemaphoreGive(sess_store_lock);
128
+    if (found) {
129
+        if (action == SESS_DROP) {
130
+            // it was dropped inside the guarded block
131
+            // the return value is not used with DROP
132
+            return NULL;
133
+        }
134
+        else if(action == SESS_GET_DATA) {
135
+            return it->data;
136
+        }
137
+    }
138
+
139
+    ESP_LOGW(TAG, "Session cookie not found: \"%s\"", cookie);
140
+    return NULL;