forked from electro/esp-irblaster
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
580 lines
20 KiB
580 lines
20 KiB
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
|
|
|
#include <esp_log.h>
|
|
#include <esp_http_server.h>
|
|
#include <sys/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, -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(sizeof(struct tpl_kv_entry), 1);
|
|
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(sizeof(struct tpl_kv_entry), 1);
|
|
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_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(sizeof(struct tpl_kv_entry), 1);
|
|
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) {
|
|
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 = calloc(BUF_CAP, 1);
|
|
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;
|
|
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) {
|
|
buf = append(buf, entry->key, &cap);
|
|
if (!buf) ESP_TRY(send_part());
|
|
buf = append(buf, "\x1f", &cap);
|
|
if (!buf) ESP_TRY(send_part());
|
|
buf = append(buf, entry->subst, &cap);
|
|
if (!buf) ESP_TRY(send_part());
|
|
if (entry->link.sle_next) {
|
|
buf = append(buf, "\x1e", &cap);
|
|
if (!buf) 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;
|
|
}
|
|
|