#define MODULE "httpd" #include "common.h" #include "fw.h" #include "httpd.h" #include #include /* * Allow the client to cache static content for this many seconds; * this improves responsiveness signficantly. */ #define HTTPD_STATIC_CACHE_AGE 300 /* 5 min */ static httpd_handle_t httpd; #define TIMEBUF_LEN 32 static const char *http_date(const struct tm *when) { static char timebuf[TIMEBUF_LEN]; strftime(timebuf, TIMEBUF_LEN, "%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) { /* * A variation on Zeller's congruence. */ unsigned int c, y, m, d; y = tm->tm_year + 1900; m = tm->tm_mon + 2; d = tm->tm_mday; if (m < 4) { m += 12; y--; } c = y/100; tm->tm_wday = (d + ((13*m)/5) + y + (y >> 2) - c + (c >> 2) + 6) % 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 esp_err_t httpd_error(httpd_req_t *req, unsigned int errcode, const char *msg) { char *header = NULL; char *body = NULL; int hlen, blen; int rv = ESP_ERR_NO_MEM; blen = asprintf(&body, "%u %s\r\n", errcode, msg); if (!body) goto out; hlen = asprintf(&header, "HTTP/1.1 %u %s\r\n" "Content-Type: text/plain; charset=\"UTF-8\"\r\n" "Content-Length: %d\r\n" "Date: %s\r\n" "Cache-Control: no-cache\r\n" "Connection: close\r\n" "\r\n", errcode, msg, blen, http_now()); if (!header) goto out; rv = httpd_send(req, header, hlen); if (!rv) rv = httpd_send(req, body, blen); out: if (header) free(header); if (body) free(body); return rv; } esp_err_t httpd_firmware_upgrade_handler(httpd_req_t *req) { char *response; esp_err_t err; int rv, len; printf("[POST] len = %zu uri = \"%s\"\n", req->content_len, req->uri); if (!req->content_len) { return httpd_error(req, 411, "Length required"); } rv = firmware_update((read_func_t)httpd_req_recv, (token_t)req); if (rv == FWUPDATE_ERR_IN_PROGRESS) return httpd_error(req, 409, "Firmware update already in progress"); else if (rv) return httpd_error(req, 500, "Firmware update failed"); len = asprintf(&response, "Firmware update completed\r\n" "Rebooting in %u seconds\r\n", reboot_delayed()); /* 200 is default, no need to set */ httpd_resp_set_type(req, "text/plain; charset=\"UTF-8\""); httpd_resp_set_hdr(req, "Date", http_now()); httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); httpd_resp_set_hdr(req, "Connection", "close"); err = httpd_resp_send(req, response, len); free(response); return err; } 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"; const 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; uri = req->uri; while (*uri == '/') uri++; /* Skip leading slashes */ enduri = strchr(uri, '\0'); if (enduri == uri) { add_index = true; } else if (enduri[-1] == '/') { add_index = true; enduri--; /* Drop terminal slash */ } else { add_index = false; /* Try the plain filename first */ } MSG("requesting: /%.*s\n", enduri - uri, uri); buffer = malloc(buffer_size); zip = malloc(sizeof *zip); if (!buffer || !zip) { err = httpd_error(req, 503, "Out of memory"); goto out; } if (enduri - uri + 1 + sizeof index_filename >= buffer_size) { err = httpd_error(req, 414, "URI too long"); goto out; } char *p = mempcpy(buffer, uri, enduri - uri); *p = '\0'; unz = unzOpen(NULL, (void *)gwwwzipData, gwwwzipSize, zip, NULL, NULL, NULL, NULL); if (!unz) { MSG("[HTTP] unzOpen failed!\n"); err = httpd_error(req, 500, "Cannot open content archive"); goto out; } while (1) { if (add_index) { if (p > buffer) *p++ = '/'; memcpy(p, index_filename, sizeof index_filename); p += sizeof index_filename - 1; /* Point to final NUL */ } MSG("trying to open: %s\n", buffer); if (unzLocateFile(unz, buffer, 1) == UNZ_OK) break; if (add_index) { err = httpd_error(req, 404, "File not found"); goto out; } add_index = true; /* Try again with the index filename */ } /* Note: p points to the end of the filename string */ size_t filelen = p - buffer; 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(p - 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 (req->method == HTTP_GET && 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" "ETag: %s\r\n" "Cache-Control: max-age=%d\r\n", response, http_now(), etag, HTTPD_STATIC_CACHE_AGE); 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" "Last-Modified: %s\r\n" "%s", mime_type->mime, mime_extra, fileinfo.uncompressed_size, http_dos_date(fileinfo.dosDate), skip_body ? "" : "Connection: close\r\n"); } if (len >= buffer_size-2) { err = httpd_resp_send_err(req, 500, "buffer_size too small"); goto out; } buffer[len++] = '\r'; buffer[len++] = '\n'; err = httpd_send(req, buffer, len); if (skip_body || err != len) { /* 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(req, buffer, chunk); if (err != chunk) 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; } static const httpd_uri_t uri_handlers[] = { { .uri = "/*", .method = HTTP_GET, .handler = httpd_static_handler, .user_ctx = NULL }, { .uri = "/*", .method = HTTP_HEAD, .handler = httpd_static_handler, .user_ctx = NULL }, { .uri = "/fwupdate/?", .method = HTTP_POST, .handler = httpd_firmware_upgrade_handler, .user_ctx = NULL } }; void my_httpd_stop(void) { if (httpd) { 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; 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_wildcard; if (httpd_start(&server, &config) != ESP_OK) return; httpd = server; for (size_t i = 0; i < ARRAY_SIZE(uri_handlers); i++) httpd_register_uri_handler(httpd, &uri_handlers[i]); printf("[HTTP] httpd started\n"); }