httpd.c 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. #define MODULE "httpd"
  2. #include "common.h"
  3. #include "fw.h"
  4. #include "httpd.h"
  5. #include <incbin.h>
  6. #include <unzipLIB.h>
  7. /*
  8. * Allow the client to cache static content for this many seconds;
  9. * this improves responsiveness signficantly.
  10. */
  11. #define HTTPD_STATIC_CACHE_AGE 300 /* 5 min */
  12. static httpd_handle_t httpd;
  13. #define TIMEBUF_LEN 32
  14. static const char *http_date(const struct tm *when)
  15. {
  16. static char timebuf[TIMEBUF_LEN];
  17. strftime(timebuf, TIMEBUF_LEN,
  18. "%a, %d %b %Y %H:%M:%S GMT", when);
  19. return timebuf;
  20. }
  21. static const char *http_now(void)
  22. {
  23. time_t t = time(NULL);
  24. return http_date(gmtime(&t));
  25. }
  26. static struct tm *set_weekday(struct tm *tm)
  27. {
  28. /*
  29. * A variation on Zeller's congruence.
  30. */
  31. unsigned int c, y, m, d;
  32. y = tm->tm_year + 1900;
  33. m = tm->tm_mon + 2;
  34. d = tm->tm_mday;
  35. if (m < 4) {
  36. m += 12;
  37. y--;
  38. }
  39. c = y/100;
  40. tm->tm_wday = (d + ((13*m)/5) + y + (y >> 2) - c + (c >> 2) + 6) % 7;
  41. return tm;
  42. }
  43. static const char *http_dos_date(uint32_t dos_date)
  44. {
  45. struct tm tm;
  46. tm.tm_sec = (dos_date << 1) & 63;
  47. tm.tm_min = (dos_date >> 5) & 63;
  48. tm.tm_hour = (dos_date >> 11) & 31;
  49. tm.tm_mday = (dos_date >> 16) & 31;
  50. tm.tm_mon = ((dos_date >> 21) & 15) - 1;
  51. tm.tm_year = (dos_date >> 25) + 80;
  52. tm.tm_isdst = 0; /* Times are stored in GMT */
  53. return http_date(set_weekday(&tm));
  54. }
  55. static esp_err_t httpd_error(httpd_req_t *req,
  56. unsigned int errcode, const char *msg)
  57. {
  58. char *header = NULL;
  59. char *body = NULL;
  60. int hlen, blen;
  61. int rv = ESP_ERR_NO_MEM;
  62. blen = asprintf(&body,
  63. "<!DOCTYPE html>\r\n"
  64. "<html>\r\n"
  65. "<head>\r\n"
  66. "<title>Error %u: %s</title>\r\n"
  67. "</head>\r\n"
  68. "<body>\r\n"
  69. "<h1>Error %u</h1>\r\n"
  70. "<p>%s</p>\r\n"
  71. "</body>\r\n"
  72. "</html>\r\n",
  73. errcode, msg, errcode, msg);
  74. if (!body)
  75. goto out;
  76. hlen = asprintf(&header,
  77. "HTTP/1.1 %u %s\r\n"
  78. "Content-Type: text/html; charset=\"UTF-8\"\r\n"
  79. "Content-Length: %d\r\n"
  80. "Date: %s\r\n"
  81. "Cache-Control: no-cache\r\n"
  82. "Connection: close\r\n"
  83. "\r\n",
  84. errcode, msg, blen, http_now());
  85. if (!header)
  86. goto out;
  87. rv = httpd_send(req, header, hlen);
  88. if (!rv)
  89. rv = httpd_send(req, body, blen);
  90. out:
  91. if (header)
  92. free(header);
  93. if (body)
  94. free(body);
  95. return rv;
  96. }
  97. esp_err_t httpd_firmware_upgrade_handler(httpd_req_t *req)
  98. {
  99. char *response;
  100. esp_err_t err;
  101. int rv, len;
  102. printf("[POST] len = %zu uri = \"%s\"\n",
  103. req->content_len, req->uri);
  104. if (!req->content_len) {
  105. return httpd_error(req, 411, "Length required");
  106. }
  107. rv = firmware_update((read_func_t)httpd_req_recv, (token_t)req);
  108. if (rv == FWUPDATE_ERR_IN_PROGRESS)
  109. return httpd_error(req, 409, "Firmware update already in progress");
  110. else if (rv)
  111. return httpd_error(req, 500, "Firmware update failed");
  112. len = asprintf(&response,
  113. "<!DOCTYPE html>\r\n"
  114. "<html>\r\n"
  115. "<head>\r\n"
  116. "<title>Firmware update completed</title>\r\n"
  117. "</head>\r\n"
  118. "<body>\r\n"
  119. "<h1>Firmware update completed</h1>\r\n"
  120. "<p>Rebooting in %u seconds</p>\r\n"
  121. "</body>\r\n"
  122. "</html>\r\n",
  123. reboot_delayed());
  124. /* 200 and text/html are the response defaults, no need to set */
  125. httpd_resp_set_hdr(req, "Date", http_now());
  126. httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
  127. httpd_resp_set_hdr(req, "Connection", "close");
  128. err = httpd_resp_send(req, response, len);
  129. free(response);
  130. return err;
  131. }
  132. INCBIN_EXTERN(wwwzip);
  133. struct mime_type {
  134. const char *ext;
  135. uint16_t ext_len;
  136. uint16_t flags;
  137. const char *mime;
  138. };
  139. #define MT_CHARSET 1 /* Add charset to Content-Type */
  140. static const struct mime_type mime_types[] = {
  141. { ".html", 5, MT_CHARSET, "text/html" },
  142. { ".xhtml", 6, MT_CHARSET, "text/html" },
  143. { ".css", 4, MT_CHARSET, "text/css" },
  144. { ".webp", 5, 0, "image/webp" },
  145. { ".jpg", 4, 0, "image/jpeg" },
  146. { ".png", 4, 0, "image/png" },
  147. { ".ico", 4, 0, "image/png" }, /* favicon.ico */
  148. { ".svg", 4, MT_CHARSET, "image/svg+xml" },
  149. { ".pdf", 4, 0, "application/pdf" },
  150. { ".js", 3, MT_CHARSET, "text/javascript" },
  151. { ".mjs", 4, MT_CHARSET, "text/javascript" },
  152. { ".json", 5, MT_CHARSET, "application/json" },
  153. { ".xml", 4, MT_CHARSET, "text/xml" },
  154. { ".bin", 4, 0, "application/octet-stream" },
  155. { ".fw", 3, 0, "application/octet-stream" },
  156. { NULL, 0, MT_CHARSET, "text/plain" } /* default */
  157. };
  158. static esp_err_t httpd_static_handler(httpd_req_t *req)
  159. {
  160. static const char index_filename[] = "index.html";
  161. const size_t buffer_size = UNZ_BUFSIZE;
  162. const char *uri, *enduri;
  163. bool add_index;
  164. char *buffer = NULL;
  165. ZIPFILE *zip = NULL;
  166. unzFile unz = NULL;
  167. bool file_open = false;
  168. int err = 0;
  169. size_t len;
  170. uri = req->uri;
  171. while (*uri == '/')
  172. uri++; /* Skip leading slashes */
  173. enduri = strchr(uri, '\0');
  174. if (enduri == uri) {
  175. add_index = true;
  176. } else if (enduri[-1] == '/') {
  177. add_index = true;
  178. enduri--; /* Drop terminal slash */
  179. } else {
  180. add_index = false; /* Try the plain filename first */
  181. }
  182. MSG("requesting: /%.*s\n", enduri - uri, uri);
  183. buffer = malloc(buffer_size);
  184. zip = malloc(sizeof *zip);
  185. if (!buffer || !zip) {
  186. err = httpd_error(req, 503, "Out of memory");
  187. goto out;
  188. }
  189. if (enduri - uri + 1 + sizeof index_filename >= buffer_size) {
  190. err = httpd_error(req, 414, "URI too long");
  191. goto out;
  192. }
  193. char *p = mempcpy(buffer, uri, enduri - uri);
  194. *p = '\0';
  195. unz = unzOpen(NULL, (void *)gwwwzipData, gwwwzipSize,
  196. zip, NULL, NULL, NULL, NULL);
  197. if (!unz) {
  198. MSG("[HTTP] unzOpen failed!\n");
  199. err = httpd_error(req, 500, "Cannot open content archive");
  200. goto out;
  201. }
  202. while (1) {
  203. if (add_index) {
  204. if (p > buffer)
  205. *p++ = '/';
  206. memcpy(p, index_filename, sizeof index_filename);
  207. p += sizeof index_filename - 1; /* Point to final NUL */
  208. }
  209. MSG("trying to open: %s\n", buffer);
  210. if (unzLocateFile(unz, buffer, 1) == UNZ_OK)
  211. break;
  212. if (add_index) {
  213. err = httpd_error(req, 404, "File not found");
  214. goto out;
  215. }
  216. add_index = true; /* Try again with the index filename */
  217. }
  218. /* Note: p points to the end of the filename string */
  219. size_t filelen = p - buffer;
  220. const struct mime_type *mime_type = mime_types;
  221. /* The default entry with length 0 will always match */
  222. while (mime_type->ext_len) {
  223. len = mime_type->ext_len;
  224. if (len < filelen && !memcmp(p - len, mime_type->ext, len))
  225. break;
  226. mime_type++;
  227. }
  228. unz_file_info fileinfo;
  229. memset(&fileinfo, 0, sizeof fileinfo);
  230. unzGetCurrentFileInfo(unz, &fileinfo, NULL, 0, NULL, 0, NULL, 0);
  231. /*
  232. * Hopefully the combination of date and CRC
  233. * is strong enough to quality for a "strong" ETag
  234. */
  235. char etag[16+3+1];
  236. snprintf(etag, sizeof etag, "\"%08x:%08x\"",
  237. fileinfo.dosDate, fileinfo.crc);
  238. bool skip_body = req->method == HTTP_HEAD || !fileinfo.uncompressed_size;
  239. bool skip_meta = false;
  240. const char *response = "200 OK";
  241. if (req->method == HTTP_GET &&
  242. httpd_req_get_hdr_value_str(req, "If-None-Match",
  243. buffer, buffer_size) == ESP_OK &&
  244. strstr(buffer, etag)) {
  245. skip_body = skip_meta = true;
  246. response = "304 Not Modified";
  247. }
  248. len = snprintf(buffer, buffer_size-2,
  249. "HTTP/1.1 %s\r\n"
  250. "Date: %s\r\n"
  251. "ETag: %s\r\n"
  252. "Cache-Control: max-age=%d\r\n",
  253. response,
  254. http_now(),
  255. etag,
  256. HTTPD_STATIC_CACHE_AGE);
  257. if (len < buffer_size-2 && !skip_meta) {
  258. const char *mime_extra =
  259. mime_type->flags & MT_CHARSET ? "; charset=\"UTF-8\"" : "";
  260. len += snprintf(buffer + len, buffer_size-2 - len,
  261. "Content-Type: %s%s\r\n"
  262. "Content-Length: %u\r\n"
  263. "Allow: GET, HEAD\r\n"
  264. "Last-Modified: %s\r\n"
  265. "%s",
  266. mime_type->mime, mime_extra,
  267. fileinfo.uncompressed_size,
  268. http_dos_date(fileinfo.dosDate),
  269. skip_body ? "" : "Connection: close\r\n");
  270. }
  271. if (len >= buffer_size-2) {
  272. err = httpd_resp_send_err(req, 500, "buffer_size too small");
  273. goto out;
  274. }
  275. buffer[len++] = '\r';
  276. buffer[len++] = '\n';
  277. err = httpd_send(req, buffer, len);
  278. if (skip_body || err != len) {
  279. /* No need to spend time uncompressing the file content */
  280. goto out;
  281. }
  282. if (unzOpenCurrentFile(unz) != UNZ_OK) {
  283. err = httpd_resp_send_err(req, 500, "Cannot open file in archive");
  284. goto out;
  285. }
  286. file_open = true;
  287. len = fileinfo.uncompressed_size;
  288. while (len) {
  289. size_t chunk = len;
  290. if (chunk > buffer_size)
  291. chunk = buffer_size;
  292. if (unzReadCurrentFile(unz, buffer, chunk) != chunk) {
  293. err = ESP_ERR_HTTPD_RESULT_TRUNC;
  294. goto out;
  295. }
  296. err = httpd_send(req, buffer, chunk);
  297. if (err != chunk)
  298. goto out;
  299. len -= chunk;
  300. }
  301. err = ESP_OK; /* All good! */
  302. out:
  303. if (file_open)
  304. unzCloseCurrentFile(unz);
  305. if (unz)
  306. unzClose(unz);
  307. if (zip)
  308. free(zip);
  309. if (buffer)
  310. free(buffer);
  311. return err;
  312. }
  313. static const httpd_uri_t uri_handlers[] = {
  314. {
  315. .uri = "/*",
  316. .method = HTTP_GET,
  317. .handler = httpd_static_handler,
  318. .user_ctx = NULL
  319. },
  320. {
  321. .uri = "/*",
  322. .method = HTTP_HEAD,
  323. .handler = httpd_static_handler,
  324. .user_ctx = NULL
  325. },
  326. {
  327. .uri = "/fwupdate/?",
  328. .method = HTTP_POST,
  329. .handler = httpd_firmware_upgrade_handler,
  330. .user_ctx = NULL
  331. }
  332. };
  333. void my_httpd_stop(void)
  334. {
  335. if (httpd) {
  336. httpd_stop(httpd);
  337. httpd = NULL;
  338. }
  339. }
  340. void my_httpd_start(void)
  341. {
  342. httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  343. httpd_handle_t server;
  344. if (httpd)
  345. return;
  346. config.task_priority = 2;
  347. printf("[HTTP] Default stack size: %zu\n", config.stack_size);
  348. config.stack_size <<= 2;
  349. printf("[HTTP] Requesting stack size: %zu\n", config.stack_size);
  350. config.uri_match_fn = httpd_uri_match_wildcard;
  351. if (httpd_start(&server, &config) != ESP_OK)
  352. return;
  353. httpd = server;
  354. for (size_t i = 0; i < ARRAY_SIZE(uri_handlers); i++)
  355. httpd_register_uri_handler(httpd, &uri_handlers[i]);
  356. printf("[HTTP] httpd started\n");
  357. }