#define MODULE "httpd" #include "common.h" #include "fw.h" #include "httpd.h" #include "config.h" #include #include static httpd_handle_t httpd; /* 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\""; enum hsp_flags { HSP_CLOSE = 1, HSP_CRLF = 2, HSP_CLOSE_SOCKET = 4 }; static void httpd_print_request(const httpd_req_t *req) { printf("[HTTP] %s %s\n", http_method_str(req->method), req->uri); } 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(); if (rcode > 499) flags |= HSP_CLOSE; const char *closer = flags & HSP_CLOSE ? "Connection: close\r\n" : ""; bool redirect = rcode >= 300 && rcode <= 399; static const char refresher[] = "Refresh: %u;url=/"; char refresh_str[sizeof(refresher)-2 + sizeof(unsigned int)*3]; refresh_str[0] = '\0'; if (refresh) snprintf(refresh_str, sizeof refresh_str, refresher, refresh); if (redirect) { size_t blenadj = sizeof("3xx Redirect \r"); /* \0 -> \n so don't include it */ flags |= HSP_CRLF; 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" "\r\n" "%3u Redirect ", rcode, text_plain, blen + blenadj, now, (int)blen, body, closer, refresh_str, 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-store\r\n" "Date: %s\r\n" "%s%s" "\r\n", rcode, text_plain, blen + blenadj, now, closer, refresh_str); } 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); /* Sending ESP_FAIL causes the socket to be immediately closed */ return err ? err : (flags & HSP_CLOSE_SOCKET) ? ESP_FAIL : ESP_OK; } #define HTTP_ERR(r,e,s) httpd_send_plain((r), (e), s, sizeof(s)-1, 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_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_CLOSE_SOCKET|HSP_CRLF, reboot_time+5); if (response) free(response); return rv; } static esp_err_t httpd_firmware_update(httpd_req_t *req) { int rv; /* XXX: use httpd_fopen_read() here */ rv = firmware_update((read_func_t)httpd_req_recv, (token_t)req); 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 esp_err_t httpd_set_lang(httpd_req_t *req, const char *query) { if (query && *query) { setenv_config("LANG", query); read_config(NULL, true); /* Save configuration */ } return httpd_send_plain(req, 302, "/", 1, 0, 0); } #define STRING_MATCHES(str, len, what) \ (((len) == sizeof(what)-1) && !memcmp((str), (what), sizeof(what)-1)) static esp_err_t httpd_sys_post_handler(httpd_req_t *req) { httpd_print_request(req); if (!httpd_req_get_hdr_value_len(req, "Content-Length")) return HTTP_ERR(req, 411, "Length required"); if (strncmp(req->uri, "/sys/", 5)) return httpd_err_enoent(req); /* This should never happen */ const char *file = req->uri + 5; size_t filelen = strcspn(file, "?"); const char *query = NULL; if (file[filelen] == '?') query = &file[filelen] + 1; 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, "lang")) return httpd_set_lang(req, query); return httpd_err_enoent(req); } static esp_err_t httpd_get_config(httpd_req_t *req) { 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-store"); int rv = write_config(f); fclose(f); return rv ? ESP_FAIL : ESP_OK; } static esp_err_t httpd_sys_get_handler(httpd_req_t *req) { httpd_print_request(req); if (!strcmp(req->uri, "/sys/getconfig")) return httpd_get_config(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 */ 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" }, { NULL, 0, MT_CHARSET, "text/plain" } /* default */ }; static esp_err_t httpd_static_handler(httpd_req_t *req) { static const char index_filename[] = "index.html"; static const char fallback_lang[] = "en"; size_t buffer_size = UNZ_BUFSIZE; const char *uri, *enduri; bool add_index; 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) { add_index = true; } else if (last_slash == enduri-1) { add_index = true; enduri--; /* Drop terminal slash */ if (first_slash == last_slash) first_slash = NULL; } else { add_index = false; /* Try the plain filename first */ } const char *lang = getenv_def("LANG", ""); const size_t lang_size = lang ? strlen(lang)+1 : 0; const size_t lang_space = (lang_size < sizeof fallback_lang ? sizeof fallback_lang : lang_size) + 1; const size_t filename_buffer_size = (enduri - uri) + lang_space + 2 + sizeof index_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 + lang_space; 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 *filename, *endfile; unsigned int m; bool found = false; for (m = add_index; m < 6; m += (add_index+1)) { if (m & 1) { char *sx = endbase - (endbase[-1] == '/'); *sx++ = '/'; endfile = mempcpy(sx, index_filename, sizeof index_filename) - 1; } else { endfile = endbase; } *endfile = '\0'; switch (m >> 1) { case 1: if (!lang) { filename = NULL; } else { filename = filebase - lang_size; memcpy(filename, lang, lang_size-1); } break; case 2: filename = filebase - sizeof fallback_lang; memcpy(filename, fallback_lang, sizeof fallback_lang - 1); break; default: filename = filebase; break; } if (!filename) continue; filename[-1] = '/'; MSG("trying to open: %s\n", filename); if (unzLocateFile(unz, filename, 1) == UNZ_OK) { found = true; break; } } size_t filelen = endfile - filename; if (!found) { err = httpd_err_enoent(req); goto out; } else if (m) { err = httpd_send_plain(req, 302 - (m == 1), filename-1, filelen+1, 0, 0); goto out; } /* Note: p points to the end of the filename string */ 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++; } unz_file_info fileinfo; memset(&fileinfo, 0, sizeof fileinfo); unzGetCurrentFileInfo(unz, &fileinfo, NULL, 0, NULL, 0, NULL, 0); /* * 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\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 = httpd_resp_send_err(req, 500, "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_get_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_post_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 = 2; config.max_open_sockets = 8; 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"); }