#define MODULE "httpd" #include "common.h" #include "fw.h" #include "httpd.h" #include "config.h" #include "boardinfo_esp.h" #include #include #define HTTPD_PRIORITY 4 static httpd_handle_t httpd; static const char fallback_language[] = "en"; /* For unknown language */ static const char redir_filename[] = "_redir"; /* For a directory "file" */ #define MAX_LANG_LEN 16 /* Looping version of httpd_send(); this is a hidden function in the server */ static esp_err_t httpd_send_all(httpd_req_t *req, const void *buf, size_t len) { const char *p = buf; while (len) { int sent = httpd_send(req, p, len); if (sent <= 0) return ESP_ERR_HTTPD_RESP_SEND; p += sent; len -= sent; } return ESP_OK; } /* Create a file pointer from an http request */ static ssize_t httpd_io_read(void *cookie, char *buf, size_t n) { int rv = httpd_req_recv(cookie, buf, n); return rv < 0 ? -1 : rv; } static ssize_t httpd_io_write(void *cookie, const char *buf, size_t n) { return httpd_resp_send_chunk(cookie, buf, n) ? 0 : n; } static int httpd_io_close_write(void *cookie) { return httpd_resp_send_chunk(cookie, NULL, 0) ? -1 : 0; } static FILE *httpd_fopen_read(httpd_req_t *req) { static const cookie_io_functions_t http_io_read_funcs = { .read = httpd_io_read, .write = NULL, /* Not writeable */ .seek = NULL, /* Not seekable */ .close = NULL, }; return fopencookie((void *)req, "r", http_io_read_funcs); } static FILE *httpd_fopen_write(httpd_req_t *req) { static const cookie_io_functions_t http_io_write_funcs = { .read = httpd_io_read, .write = httpd_io_write, .seek = NULL, /* Not seekable */ .close = httpd_io_close_write }; return fopencookie((void *)req, "r+", http_io_write_funcs); } #define TIMEBUF_LEN 32 static const char *http_date(const struct tm *when) { static char timebuf[32]; strftime(timebuf, sizeof timebuf, "%a, %d %b %Y %H:%M:%S GMT", when); return timebuf; } static const char *http_now(void) { time_t t = time(NULL); return http_date(gmtime(&t)); } static struct tm *set_weekday(struct tm *tm) { /* * This is a variation on Zeller's congruence with a table lookup * for the month. The table contains the number of days since March 1, * mod 7 (for Jan and Feb, from March 1 of the *previous year*.) * * Sample test cases: * Wed Mar 1 0000 * Thu Jan 1 1970 * Wed Apr 27 2022 * Mon Feb 28 2000 * Wed Mar 1 2000 * Sun Feb 28 2100 * Mon Mar 1 2100 */ static const uint8_t md[12] = { 5, 1, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2 }; unsigned int c, y, m, d; y = tm->tm_year + 1900; m = tm->tm_mon; d = tm->tm_mday; if (m < 2) y--; /* Jan, Feb */ c = y/100; /* * 2 represents the base date of Tue Feb 29 0000 * * 0 = Sun, 6 = Sat */ tm->tm_wday = (d + md[m] + y + (y >> 2) - c + (c >> 2) + 2) % 7; return tm; } static const char *http_dos_date(uint32_t dos_date) { struct tm tm; tm.tm_sec = (dos_date << 1) & 63; tm.tm_min = (dos_date >> 5) & 63; tm.tm_hour = (dos_date >> 11) & 31; tm.tm_mday = (dos_date >> 16) & 31; tm.tm_mon = ((dos_date >> 21) & 15) - 1; tm.tm_year = (dos_date >> 25) + 80; tm.tm_isdst = 0; /* Times are stored in GMT */ return http_date(set_weekday(&tm)); } static const char text_plain[] = "text/plain; charset=\"UTF-8\""; static void httpd_print_request(const httpd_req_t *req) { printf("[HTTP] %s %s\n", http_method_str(req->method), req->uri); } static char *httpd_req_get_hdr(httpd_req_t *req, const char *field, size_t *lenp) { size_t len = httpd_req_get_hdr_value_len(req, field); char *val = NULL; if (len) { val = malloc(len+1); if (val) { httpd_req_get_hdr_value_str(req, field, val, len+1); val[len] = '\0'; } } if (lenp) *lenp = len; return val; } enum hsp_flags { HSP_CRLF = 1, /* Append CR LF to the body */ HSP_CLOSE = 2, /* Add a Connection: close header */ HSP_REFERER = 4, /* Use referer as body (for redirects) */ HSP_UNCACHE = 8 /* Wipe caches, please... */ }; static esp_err_t httpd_send_plain(httpd_req_t *req, unsigned int rcode, const char *body, size_t blen, enum hsp_flags flags, unsigned int refresh) { char *header = NULL; esp_err_t err; int hlen; const char *now = http_now(); MSG("http_send_plain %u \"%.*s\"\n", rcode, (int)blen, body); if (rcode > 499) flags |= HSP_CLOSE; const char *closer = flags & HSP_CLOSE ? "Connection: close\r\n" : ""; bool redirect = rcode >= 300 && rcode <= 399; char *referer = NULL; char *refresher_buf = NULL; if (refresh || (flags & HSP_REFERER)) { size_t referer_len; referer = httpd_req_get_hdr(req, "Referer", &referer_len); /* "Effective" referer */ const char * const ereferer = referer ? referer : "/"; if (refresh) { asprintf(&refresher_buf, "Refresh: %u;url=%s\r\n", refresh, ereferer); } if (flags & HSP_REFERER) { body = ereferer; blen = referer ? referer_len : 1; } } const char * const refresher = refresher_buf ? refresher_buf : ""; const char * const uncacher = (flags & HSP_UNCACHE) ? "Clear-Site-Data: \"cache\"\r\n" : ""; if (redirect) { /* \0 -> \n so don't include it */ size_t blenadj = sizeof("3xx Redirect \r"); /* Always CR LF */ flags |= HSP_CRLF; /* Drop any CR LF already in the redirect string */ for (size_t bchk = 0; bchk < blen; bchk++) { if (body[bchk] == '\r' || body[bchk] == '\n') { blen = bchk; break; } } hlen = asprintf(&header, "HTTP/1.1 %u\r\n" "Content-Type: %s\r\n" "Content-Length: %zu\r\n" "Date %s\r\n" "Location: %.*s\r\n" "%s%s%s" "\r\n" "%3u Redirect ", rcode, text_plain, blen + blenadj, now, (int)blen, body, closer, refresher, uncacher, rcode); } else { size_t blenadj = (flags & HSP_CRLF) ? 2 : 0; hlen = asprintf(&header, "HTTP/1.1 %u\r\n" "Content-Type: %s\r\n" "Content-Length: %zu\r\n" "Cache-Control: no-cache\r\n" "Date: %s\r\n" "%s%s%s" "\r\n", rcode, text_plain, blen + blenadj, now, closer, refresher, uncacher); } if (refresher_buf) free(refresher_buf); if (referer) free(referer); if (!header) return ESP_ERR_NO_MEM; err = httpd_send_all(req, header, hlen); if (!err && blen) { err = httpd_send_all(req, body, blen); } if (!err && (flags & HSP_CRLF)) { err = httpd_send_all(req, "\r\n", 2); } if (header) free(header); return err; } #define SL(s) (s), (sizeof(s)-1) #define HTTP_ERR(r,e,s) httpd_send_plain((r), (e), SL(s), HSP_CRLF, 0) static esp_err_t httpd_err_enoent(httpd_req_t *req) { return HTTP_ERR(req, 404, "URI not found"); } static esp_err_t httpd_send_ok(httpd_req_t *req) { return HTTP_ERR(req, 200, "OK"); } static esp_err_t httpd_err_enomem(httpd_req_t *req) { return HTTP_ERR(req, 503, "Out of memory"); } static esp_err_t httpd_err_not_post(httpd_req_t *req) { return HTTP_ERR(req, 405, "Only POST allowed"); } #define HTTPD_ASSERT_POST(req) \ do { \ if ((req)->method != HTTP_POST) \ return httpd_err_not_post(req); \ } while (0) static esp_err_t httpd_update_done(httpd_req_t *req, const char *what, int err) { char *response = NULL; int len; unsigned int reboot_time = reboot_delayed(); if (err) { len = asprintf(&response, "%s update failed: %s\r\n" "Rebooting in %u seconds\r\n", what, firmware_errstr(err), reboot_time); } else { len = asprintf(&response, "%s update complete\r\n" "Rebooting in %u seconds\r\n", what, reboot_time); } if (!response) len = 0; esp_err_t rv = httpd_send_plain(req, err ? 400 : 200, response, len, HSP_CLOSE|HSP_UNCACHE, reboot_time+5); if (response) free(response); return rv; } static esp_err_t httpd_firmware_update(httpd_req_t *req) { int rv; HTTPD_ASSERT_POST(req); /* XXX: use httpd_fopen_read() here */ rv = firmware_update_start((read_func_t)httpd_req_recv, (token_t)req, false); if (!rv) rv = firmware_update_wait(portMAX_DELAY); return httpd_update_done(req, "Firmware", rv); } static esp_err_t httpd_set_config(httpd_req_t *req, const char *query) { FILE *f; size_t qlen; int rv1 = 0; if (query) { rv1 = set_config_url_string(query); } int rv2 = 0; f = NULL; if (req->content_len) { f = httpd_fopen_read(req); if (!f) return HTTP_ERR(req, 500, "Unable to get request handle"); } rv2 = read_config(f, true); if (f) fclose(f); return httpd_update_done(req, "Configuration", rv1 ? rv1 : rv2); } static inline bool is_eol(int c) { return c == EOF || c == '\0' || c == '\r' || c == '\n'; } static esp_err_t httpd_set_board_rev(httpd_req_t *req) { FILE *f = NULL; static const char rev_prefix[] = "max80.hw.ver"; static const char rev_valid[] = "MAX80 v"; char *rev_str; int err = Z_DATA_ERROR; enum sbr_parse_state { ps_start, ps_prefix, ps_string, ps_skipline }; enum sbr_parse_state state; const char *match_ptr = NULL; char *p = NULL; int c; HTTPD_ASSERT_POST(req); rev_str = malloc(sizeof board_info.version_str); if (!rev_str) return httpd_err_enomem(req); f = httpd_fopen_read(req); if (!f) { free(rev_str); return HTTP_ERR(req, 500, "Unable to get request handle"); } state = ps_start; do { bool eol; c = getc(f); eol = is_eol(c); switch (state) { case ps_start: match_ptr = rev_prefix; state = ps_prefix; /* fall through */ case ps_prefix: if (eol) { state = ps_start; } else if (*match_ptr && c == *match_ptr) { match_ptr++; } else if (!*match_ptr && c == '=') { p = rev_str; state = ps_string; } else { state = ps_skipline; } break; case ps_string: if (eol) { *p = '\0'; if (!memcmp(rev_str, rev_valid, sizeof rev_valid - 1)) { /* Otherwise input truncated or invalid */ printf("[HTTP] setting board revision: %s\n", rev_str); if (!board_info_set(rev_str)) { setenv_cond("status.max80.hw.ver", board_info.version_str); err = Z_OK; } else { err = FWUPDATE_ERR_CONFIG_SAVE; } } state = ps_start; } else if (p - rev_str >= sizeof board_info.version_str - 1) { state = ps_skipline; } else { *p++ = c; } break; case ps_skipline: if (eol) state = ps_start; break; } } while (c != EOF); fclose(f); free(rev_str); return httpd_update_done(req, "board revision", err); } #define MIN_STATUS_REF 1 /* Minimum refresh time in s */ static void httpd_get_status_extra(FILE *f, httpd_req_t *req) { static const char refresh_time_config[] = "http.status.refresh"; char timebuf[64]; size_t len; struct timeval tv; unsigned long statref; statref = Max(getenv_ul(refresh_time_config, 0), MIN_STATUS_REF); if (httpd_req_get_url_query_str(req, timebuf, sizeof timebuf) == ESP_OK && *timebuf) { char *ep; unsigned long newstatref = strtoul(timebuf, &ep, 10); if (!*ep && newstatref >= MIN_STATUS_REF && newstatref != statref) { statref = newstatref; setenv_config(refresh_time_config, timebuf); read_config(NULL, true); /* Save changed config */ } } fprintf(f, "%s=%lu\n", refresh_time_config, statref); gettimeofday(&tv,NULL); const struct tm *tm = localtime(&tv.tv_sec); len = strftime(timebuf, sizeof timebuf, "localtime=%Y-%m-%d %H:%M:%S.", tm); snprintf(timebuf+len, sizeof timebuf - len, "%06u", (unsigned long)tv.tv_usec); len += 3; len += strftime(timebuf+len, sizeof timebuf - len, " %z (%Z)\n", tm); fwrite(timebuf, 1, len, f); } static esp_err_t httpd_get_config_status(httpd_req_t *req, bool status) { FILE *f = httpd_fopen_write(req); if (!f) return HTTP_ERR(req, 500, "Unable to get request handle"); httpd_resp_set_type(req, text_plain); httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); if (status) httpd_get_status_extra(f, req); int rv = write_env(f, status); fclose(f); return rv ? ESP_FAIL : ESP_OK; } static esp_err_t httpd_set_lang(httpd_req_t *req, const char *query) { if (query) { int qlen = strlen(query); setenv_config("LANG", qlen && qlen <= MAX_LANG_LEN ? query : NULL); read_config(NULL, true); /* Save configuration */ } /* * 303 = "See other": proper return code saying "this is not what * you asked for, this is *about* what you asked for"; see spec * but this is exactly what we want here. */ return httpd_send_plain(req, 303, NULL, 0, HSP_REFERER, 0); } static esp_err_t httpd_get_lang(httpd_req_t *req) { const char *lang = getenv_def("LANG", ""); return httpd_send_plain(req, 200, lang, strlen(lang), HSP_CRLF, 0); } static esp_err_t httpd_lang_redirect(httpd_req_t *req) { char lang_buf[sizeof req->uri + MAX_LANG_LEN]; int len = snprintf(lang_buf, sizeof lang_buf, "/lang/%s%s", getenv_def("LANG", fallback_language), req->uri + (sizeof("/sys/lang")-1)); return httpd_send_plain(req, 302, lang_buf, len, 0, 0); } #define STRING_MATCHES(str, len, what) \ (((len) == sizeof(what)-1) && !memcmp((str), (what), sizeof(what)-1)) #define STRING_MATCHES_PREFIX(str, len, what) \ (((len) >= sizeof(what)-1) && !memcmp((str), (what), sizeof(what)-1)) static esp_err_t httpd_sys_handler(httpd_req_t *req) { httpd_print_request(req); if (req->method == HTTP_POST && !httpd_req_get_hdr_value_len(req, "Content-Length")) { return HTTP_ERR(req, 411, "Length required"); } const char *query = strchrnul(req->uri, '?'); if (query < req->uri+5 || memcmp(req->uri, "/sys/", 5)) return httpd_err_enoent(req); /* This should never happen */ const char *file = req->uri + 5; size_t filelen = query - file; query = *query == '?' ? query+1 : NULL; if (STRING_MATCHES_PREFIX(file, filelen, "lang")) return httpd_lang_redirect(req); if (STRING_MATCHES(file, filelen, "getstatus")) return httpd_get_config_status(req, true); if (STRING_MATCHES(file, filelen, "getconfig")) return httpd_get_config_status(req, false); if (STRING_MATCHES(file, filelen, "fwupdate")) return httpd_firmware_update(req); if (STRING_MATCHES(file, filelen, "setconfig")) return httpd_set_config(req, query); if (STRING_MATCHES(file, filelen, "getlang")) return httpd_get_lang(req); if (STRING_MATCHES(file, filelen, "setlang")) return httpd_set_lang(req, query); if (STRING_MATCHES(file, filelen, "setboardrev")) return httpd_set_board_rev(req); return httpd_err_enoent(req); } INCBIN_EXTERN(wwwzip); struct mime_type { const char *ext; uint16_t ext_len; uint16_t flags; const char *mime; }; #define MT_CHARSET 1 /* Add charset to Content-Type */ #define MT_REDIR 2 /* It is a redirect */ static const struct mime_type mime_types[] = { { ".html", 5, MT_CHARSET, "text/html" }, { ".xhtml", 6, MT_CHARSET, "text/html" }, { ".css", 4, MT_CHARSET, "text/css" }, { ".webp", 5, 0, "image/webp" }, { ".jpg", 4, 0, "image/jpeg" }, { ".png", 4, 0, "image/png" }, { ".ico", 4, 0, "image/png" }, /* favicon.ico */ { ".svg", 4, MT_CHARSET, "image/svg+xml" }, { ".otf", 4, 0, "font/otf" }, { ".ttf", 4, 0, "font/ttf" }, { ".woff", 5, 0, "font/woff" }, { ".woff2", 6, 0, "font/woff2" }, { ".pdf", 4, 0, "application/pdf" }, { ".js", 3, MT_CHARSET, "text/javascript" }, { ".mjs", 4, MT_CHARSET, "text/javascript" }, { ".json", 5, MT_CHARSET, "application/json" }, { ".xml", 4, MT_CHARSET, "text/xml" }, { ".bin", 4, 0, "application/octet-stream" }, { ".fw", 3, 0, "application/octet-stream" }, { ".capt", 5, 0, "application/captive+json" }, { "_redir", 6, MT_REDIR, NULL }, { NULL, 0, MT_CHARSET, "text/plain" } /* default */ }; static esp_err_t httpd_static_handler(httpd_req_t *req) { size_t buffer_size = UNZ_BUFSIZE; const char *uri, *enduri; bool is_dir; char *buffer = NULL; ZIPFILE *zip = NULL; unzFile unz = NULL; bool file_open = false; int err = 0; size_t len; httpd_print_request(req); uri = req->uri; while (*uri == '/') uri++; /* Skip leading slashes */ const char *first_slash = NULL, *last_slash = NULL; for (enduri = uri; *enduri; enduri++) { if (*enduri == '/') { last_slash = enduri; if (!first_slash) first_slash = enduri; } } if (enduri == uri) { is_dir = true; } else if (last_slash == enduri-1) { is_dir = true; enduri--; /* Drop terminal slash */ if (first_slash == last_slash) first_slash = NULL; } else { is_dir = false; /* Try the plain filename first */ } const size_t filename_buffer_size = (enduri - uri) + 2 + sizeof redir_filename; if (buffer_size < filename_buffer_size) buffer_size = filename_buffer_size; buffer = malloc(buffer_size); zip = malloc(sizeof *zip); if (!buffer || !zip) { err = httpd_err_enomem(req); goto out; } char * const filebase = buffer + 1; char * const endbase = mempcpy(filebase, uri, enduri - uri); filebase[-1] = '/'; unz = unzOpen(NULL, (void *)gwwwzipData, gwwwzipSize, zip, NULL, NULL, NULL, NULL); if (!unz) { MSG("[HTTP] unzOpen failed!\n"); err = HTTP_ERR(req, 500, "Cannot open content archive"); goto out; } char * const filename = filebase; /* Separate for future needs */ char *endfile = endbase; unsigned int m; bool found = false; for (m = is_dir ? 2 : 0; m < 3; m++) { char *sx; switch (m) { default: /* filename = /url */ endfile = endbase; break; case 1: /* filename = /url_redir */ sx = endbase; endfile = mempcpy(sx, redir_filename, sizeof redir_filename) - 1; break; case 2: /* filename = /url/_redir */ sx = endbase - (endbase[-1] == '/'); *sx++ = '/'; endfile = mempcpy(sx, redir_filename, sizeof redir_filename) - 1; break; } *endfile = '\0'; if (!filename) continue; filename[-1] = '/'; MSG("trying to open: %s... ", filename); if (unzLocateFile(unz, filename, 1) == UNZ_OK) { CMSG("found\n"); found = true; break; } else { CMSG("not found\n"); } } if (!found) { err = httpd_err_enoent(req); goto out; } size_t filelen = endfile - filename; const struct mime_type *mime_type = mime_types; /* The default entry with length 0 will always match */ while (mime_type->ext_len) { len = mime_type->ext_len; if (len <= filelen && !memcmp(endfile - len, mime_type->ext, len)) break; mime_type++; } MSG("found %s ext %s type %s\n", filename, mime_type->ext ? mime_type->ext : "(none)", mime_type->mime ? mime_type->mime : "(none)"); unz_file_info fileinfo; memset(&fileinfo, 0, sizeof fileinfo); unzGetCurrentFileInfo(unz, &fileinfo, NULL, 0, NULL, 0, NULL, 0); MSG("len %u compressed %u\n", fileinfo.uncompressed_size, fileinfo.compressed_size); /* * Is it a redirect? */ if (mime_type->flags & MT_REDIR) { if (fileinfo.uncompressed_size > buffer_size || unzOpenCurrentFile(unz) != UNZ_OK) { err = HTTP_ERR(req, 500, "Cannot open file in archive"); goto out; } file_open = true; len = fileinfo.uncompressed_size; if (unzReadCurrentFile(unz, buffer, len) != len) { err = ESP_ERR_HTTPD_RESULT_TRUNC; goto out; } MSG("redirect: %.*s\n", (int)len, buffer); err = httpd_send_plain(req, 302, buffer, len, 0, 0); goto out; } /* * Hopefully the combination of date and CRC * is strong enough to quality for a "strong" ETag */ char etag[16+3+1]; snprintf(etag, sizeof etag, "\"%08x:%08x\"", fileinfo.dosDate, fileinfo.crc); bool skip_body = req->method == HTTP_HEAD || !fileinfo.uncompressed_size; bool skip_meta = false; const char *response = "200 OK"; if (httpd_req_get_hdr_value_str(req, "If-None-Match", buffer, buffer_size) == ESP_OK && strstr(buffer, etag)) { skip_body = skip_meta = true; response = "304 Not Modified"; } len = snprintf(buffer, buffer_size-2, "HTTP/1.1 %s\r\n" "Date: %s\r\n" "Cache-Control: max-age=10, immutable\r\n" "ETag: %s\r\n", response, http_now(), etag); if (len < buffer_size-2 && !skip_meta) { const char *mime_extra = mime_type->flags & MT_CHARSET ? "; charset=\"UTF-8\"" : ""; len += snprintf(buffer + len, buffer_size-2 - len, "Content-Type: %s%s\r\n" "Content-Length: %u\r\n" "Allow: GET, HEAD\r\n" "Connection: close\r\n" "Last-Modified: %s\r\n", mime_type->mime, mime_extra, fileinfo.uncompressed_size, http_dos_date(fileinfo.dosDate)); } if (len >= buffer_size-2) { err = HTTP_ERR(req, 500, "buffer_size too small"); goto out; } buffer[len++] = '\r'; buffer[len++] = '\n'; err = httpd_send_all(req, buffer, len); if (skip_body || err) { /* No need to spend time uncompressing the file content */ goto out; } if (unzOpenCurrentFile(unz) != UNZ_OK) { err = HTTP_ERR(req, 400, "Cannot open file in archive"); goto out; } file_open = true; len = fileinfo.uncompressed_size; while (len) { size_t chunk = len; if (chunk > buffer_size) chunk = buffer_size; if (unzReadCurrentFile(unz, buffer, chunk) != chunk) { err = ESP_ERR_HTTPD_RESULT_TRUNC; goto out; } err = httpd_send_all(req, buffer, chunk); if (err) goto out; len -= chunk; } err = ESP_OK; /* All good! */ out: if (file_open) unzCloseCurrentFile(unz); if (unz) unzClose(unz); if (zip) free(zip); if (buffer) free(buffer); return err; } /* * Match a URL against a path prefix. To keep httpd from refusing to * register subpaths, the root template does not include the leading * '/', but uri is required to have it. Do not include a trailing / * in prefix; it is implied. */ static bool httpd_uri_match_prefix(const char *template, const char *uri, size_t len) { #if 0 printf("[HTTP] matching URI \"%.*s\" against template \"%s\"\n", len, uri, template); #endif if (!len-- || *uri++ != '/') return false; /* Previous template character (leading '/' implied) */ unsigned char tp = '/'; while (1) { unsigned char t = *template++; unsigned char u; if (!len-- || !(u = *uri++)) { return !t; } else if (!t) { return tp == '/' || u == '/'; } else if (t != u) { return false; } tp = t; } } /* Do not include leading or trailing /; most specific prefix first */ static const httpd_uri_t uri_handlers[] = { { .uri = "sys", .method = HTTP_GET, .handler = httpd_sys_handler, .user_ctx = NULL }, { .uri = "", .method = HTTP_GET, .handler = httpd_static_handler, .user_ctx = NULL }, { .uri = "", .method = HTTP_HEAD, .handler = httpd_static_handler, .user_ctx = NULL }, { .uri = "sys", .method = HTTP_POST, .handler = httpd_sys_handler, .user_ctx = NULL }, }; void my_httpd_stop(void) { if (httpd) { esp_unregister_shutdown_handler(my_httpd_stop); httpd_stop(httpd); httpd = NULL; } } void my_httpd_start(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_handle_t server; if (httpd) return; config.task_priority = HTTPD_PRIORITY; config.max_open_sockets = 10; printf("[HTTP] Default stack size: %zu\n", config.stack_size); config.stack_size <<= 2; printf("[HTTP] Requesting stack size: %zu\n", config.stack_size); config.uri_match_fn = httpd_uri_match_prefix; if (httpd_start(&server, &config) != ESP_OK) return; esp_register_shutdown_handler(my_httpd_stop); httpd = server; for (size_t i = 0; i < ARRAY_SIZE(uri_handlers); i++) { const httpd_uri_t * const handler = &uri_handlers[i]; if (httpd_register_uri_handler(httpd, handler)) printf("[HTTP] failed to register URI handler: %s %s\n", http_method_str(handler->method), handler->uri); } printf("[HTTP] httpd started\n"); }