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