|
1 /** |
|
2 * @file task_http.c |
|
3 * @brief HTTP and Websocket server functions. |
|
4 * This uses some ESP32 Websocket code written by Blake Felt - blake.w.felt@gmail.com |
|
5 */ |
|
6 #include "config.h" |
|
7 #include "mbedtls/base64.h" |
|
8 #include "mbedtls/sha1.h" |
|
9 |
|
10 |
|
11 const char *TAG = "task_http"; |
|
12 static QueueHandle_t client_queue; |
|
13 const static int client_queue_size = 10; |
|
14 static TaskHandle_t xTaskHTTP = NULL; |
|
15 static TaskHandle_t xTaskQueue = NULL; |
|
16 |
|
17 |
|
18 |
|
19 void dump_buf(char *buf, uint16_t buflen); |
|
20 |
|
21 |
|
22 /** |
|
23 * @brief Send HTTP error and message |
|
24 * @param conn The socket to send to. |
|
25 * @param error The HTTP error code. |
|
26 * @param message Yhe human readable explanation. |
|
27 * @paeam body The human readable explanation. |
|
28 */ |
|
29 static void http_error(struct netconn *conn, int error, char *message, char *body) |
|
30 { |
|
31 char *tmp = malloc(200); |
|
32 |
|
33 ESP_LOGI(TAG, "Http response %d - %s", error, message); |
|
34 if (strlen(body)) { |
|
35 snprintf(tmp, 199, "HTTP/1.1 %d %s\r\nContent-type: text/plain\r\n\r\n%s\n", error, message, body); |
|
36 } else { |
|
37 snprintf(tmp, 199, "HTTP/1.1 %d %s\r\n\r\n", error, message); |
|
38 } |
|
39 netconn_write(conn, tmp, strlen(tmp), NETCONN_NOCOPY); |
|
40 free(tmp); |
|
41 } |
|
42 |
|
43 |
|
44 |
|
45 /** |
|
46 * @brief Send HTTP file from the filesystem. |
|
47 * @param conn The network connection. |
|
48 * @param url The requested url. |
|
49 * @param mod_since The date/time of the file, or NULL. |
|
50 * @param ipstr The IP address of the remote. |
|
51 */ |
|
52 static void http_sendfile(struct netconn *conn, char *url, char *mod_since, char *ipstr) |
|
53 { |
|
54 char temp_url[128], temp_url_gz[128], header[128], c_type[32]; |
|
55 struct stat st; |
|
56 off_t filesize; |
|
57 size_t sentsize; |
|
58 char strftime_buf[64]; |
|
59 err_t err; |
|
60 bool send_gz; |
|
61 FILE *f; |
|
62 |
|
63 if (url[strlen(url) - 1] == '/') { |
|
64 // If no filename given, use index.html |
|
65 sprintf(temp_url, "/spiffs/w%sindex.html", url); |
|
66 } else if (strncmp(url, "/log/", 5) == 0) { |
|
67 // Logfiles are on the SD card. |
|
68 sprintf(temp_url, "/sdcard/w%s", url); |
|
69 } else { |
|
70 sprintf(temp_url, "/spiffs/w%s", url); |
|
71 for (int i = 0; i < 127; i++) { |
|
72 if (temp_url[i] == '?') |
|
73 temp_url[i] = '\0'; |
|
74 if (temp_url[i] == '\0') |
|
75 break; |
|
76 } |
|
77 } |
|
78 sprintf(temp_url_gz, "%s.gz", temp_url); |
|
79 |
|
80 /* |
|
81 * Get filesize and date for the response headers. |
|
82 * Note that the /spiffs filesystem doesn't support file timestamps |
|
83 * and returns the epoch for each file. Therefore we cannot use |
|
84 * the cache based on timestamps. |
|
85 */ |
|
86 filesize = 0; |
|
87 strftime_buf[0] = '\0'; |
|
88 send_gz = false; |
|
89 if (stat(temp_url_gz, &st) == 0) { |
|
90 filesize = st.st_size; |
|
91 strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime))); |
|
92 send_gz = true; |
|
93 } else if (stat(temp_url, &st) == 0) { |
|
94 filesize = st.st_size; |
|
95 strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", localtime(&(st.st_mtime))); |
|
96 } |
|
97 |
|
98 /* |
|
99 * If we have a If-Modified-Since parameter, compare that with the current |
|
100 * filedate on disk. If It's the same send a 304 response. |
|
101 * Cannot work on /spiffs. |
|
102 */ |
|
103 #if 0 |
|
104 time_t Now; |
|
105 struct tm timeInfo; |
|
106 if (mod_since && strlen(strftime_buf)) { |
|
107 time(&Now); |
|
108 localtime_r(&Now, &timeInfo); |
|
109 strftime(strftime_buf, sizeof(strftime_buf), "%a, %d %b %Y %T %z", &timeInfo); |
|
110 sprintf(header, "HTTP/1.1 304 Not Modified\r\nDate: %s\r\n\r\n", strftime_buf); |
|
111 netconn_write(conn, header, strlen(header), NETCONN_NOCOPY); |
|
112 ESP_LOGI(TAG, "http_sendfile %s Not Modified, ok", temp_url); |
|
113 return; |
|
114 } |
|
115 #endif |
|
116 |
|
117 if (send_gz) { |
|
118 f = fopen(temp_url_gz, "r"); |
|
119 } else { |
|
120 f = fopen(temp_url, "r"); |
|
121 } |
|
122 if (f == NULL) { |
|
123 ESP_LOGI(TAG, "%s url \'%s\' file \'%s\' not found", ipstr, url, temp_url); |
|
124 http_error(conn, 404, "Not found", "Not found"); |
|
125 return; |
|
126 } |
|
127 |
|
128 if (strcmp(".html", &temp_url[strlen(temp_url) - 5]) == 0) { |
|
129 sprintf(c_type, "text/html"); |
|
130 } else if (strcmp(".css", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
131 sprintf(c_type, "text/css"); |
|
132 } else if (strcmp(".js", &temp_url[strlen(temp_url) - 3]) == 0) { |
|
133 sprintf(c_type, "text/javascript"); |
|
134 } else if (strcmp(".json", &temp_url[strlen(temp_url) - 5]) == 0) { |
|
135 sprintf(c_type, "text/json"); |
|
136 } else if (strcmp(".gz", &temp_url[strlen(temp_url) - 3]) == 0) { |
|
137 sprintf(c_type, "application/x-gzip"); |
|
138 } else if (strcmp(".png", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
139 sprintf(c_type, "image/png"); |
|
140 } else if (strcmp(".svg", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
141 sprintf(c_type, "image/svg+xml"); |
|
142 } else if (strcmp(".oga", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
143 sprintf(c_type, "audio/ogg"); |
|
144 } else if (strcmp(".ico", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
145 sprintf(c_type, "image/x-icon"); |
|
146 } else if (strcmp(".xml", &temp_url[strlen(temp_url) - 4]) == 0) { |
|
147 sprintf(c_type, "text/xml"); |
|
148 } else { |
|
149 sprintf(c_type, "application/octet-stream"); |
|
150 printf("Unknown content type for %s\n", temp_url); |
|
151 } |
|
152 |
|
153 vTaskDelay(2 / portTICK_PERIOD_MS); |
|
154 // httpdHeader(connData, "Cache-Control", "max-age=3600, must-revalidate"); |
|
155 if (filesize) { |
|
156 sprintf(header, "HTTP/1.1 200 OK\r\nLast-Modified: %s\r\nContent-Length: %ld\r\nContent-type: %s\r\n", strftime_buf, filesize, c_type); |
|
157 } else { |
|
158 sprintf(header, "HTTP/1.1 200 OK\r\nContent-type: %s\r\n", c_type); |
|
159 } |
|
160 if (send_gz) { |
|
161 sprintf(header, "%sContent-Encoding: gzip\r\n", header); |
|
162 } |
|
163 sprintf(header, "%s\r\n", header); // Add last empty line. |
|
164 err = netconn_write(conn, header, strlen(header), NETCONN_NOCOPY); |
|
165 if (err != ERR_OK) { |
|
166 ESP_LOGE(TAG, "%s sendfile %s%s err=%d on header write", ipstr, temp_url, (send_gz) ? ".gz":"", err); |
|
167 fclose(f); |
|
168 return; |
|
169 } |
|
170 // if (strstr(acceptEncodingBuffer, "gzip") == NULL) |
|
171 // http_error(conn, 501, "Not implemented", "Your browser does not accept gzip-compressed data."); |
|
172 |
|
173 sentsize = 0; |
|
174 uint8_t *buff = malloc(1024); |
|
175 size_t bytes; |
|
176 int pause = 0; |
|
177 |
|
178 for (;;) { |
|
179 bytes = fread(buff, 1, 1024, f); |
|
180 if (bytes == 0) |
|
181 break; |
|
182 |
|
183 err = netconn_write(conn, buff, bytes, NETCONN_NOCOPY); |
|
184 if (err != ERR_OK) { |
|
185 ESP_LOGE(TAG, "%s sendfile %s%s err=%d send %u bytes of %ld bytes", ipstr, temp_url, (send_gz) ? ".gz":"", err, sentsize, filesize); |
|
186 break; |
|
187 } |
|
188 vTaskDelay(2 / portTICK_PERIOD_MS); |
|
189 sentsize += bytes; |
|
190 pause++; |
|
191 if (pause > 50) { // 50 K |
|
192 pause = 0; |
|
193 vTaskDelay(50 / portTICK_PERIOD_MS); |
|
194 } |
|
195 } |
|
196 fclose(f); |
|
197 free(buff); |
|
198 |
|
199 if (sentsize == filesize) { |
|
200 ESP_LOGI(TAG, "%s sendfile %s%s sent %u bytes, ok (%s)", ipstr, temp_url, (send_gz) ? ".gz":"", sentsize, url); |
|
201 } |
|
202 } |
|
203 |
|
204 |
|
205 |
|
206 /** |
|
207 * @brief Handle VNC websocket events. |
|
208 */ |
|
209 void websockify_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len) |
|
210 { |
|
211 switch(type) { |
|
212 case WEBSOCKET_CONNECT: |
|
213 ESP_LOGI(TAG,"Websockify client %i connected!",num); |
|
214 break; |
|
215 |
|
216 case WEBSOCKET_DISCONNECT_EXTERNAL: |
|
217 ESP_LOGI(TAG,"Websockify client %i sent a disconnect message",num); |
|
218 VncStopWS(num); |
|
219 break; |
|
220 |
|
221 case WEBSOCKET_DISCONNECT_INTERNAL: |
|
222 ESP_LOGI(TAG,"Websockify client %i was disconnected",num); |
|
223 VncStopWS(num); |
|
224 break; |
|
225 |
|
226 case WEBSOCKET_DISCONNECT_ERROR: |
|
227 ESP_LOGI(TAG,"Websockify client %i was disconnected due to an error",num); |
|
228 VncStopWS(num); |
|
229 break; |
|
230 |
|
231 case WEBSOCKET_TEXT: |
|
232 ESP_LOGI(TAG,"Websockiify client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg); |
|
233 break; |
|
234 |
|
235 case WEBSOCKET_BIN: |
|
236 VncGetWSmessage(msg, len); |
|
237 //dump_buf(msg, len); |
|
238 break; |
|
239 |
|
240 case WEBSOCKET_PING: |
|
241 ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(uint32_t)len,msg); |
|
242 break; |
|
243 |
|
244 case WEBSOCKET_PONG: |
|
245 ESP_LOGI(TAG,"client %i responded to the ping",num); |
|
246 break; |
|
247 } |
|
248 } |
|
249 |
|
250 |
|
251 |
|
252 /** |
|
253 * @brief Handle web ui websocket events. |
|
254 */ |
|
255 void websock_callback(uint8_t num, WEBSOCKET_TYPE_t type, char* msg, uint64_t len) |
|
256 { |
|
257 switch(type) { |
|
258 case WEBSOCKET_CONNECT: |
|
259 ESP_LOGI(TAG,"Websocket client %i connected!",num); |
|
260 break; |
|
261 |
|
262 case WEBSOCKET_DISCONNECT_EXTERNAL: |
|
263 ESP_LOGI(TAG,"Websocket client %i sent a disconnect message",num); |
|
264 break; |
|
265 |
|
266 case WEBSOCKET_DISCONNECT_INTERNAL: |
|
267 ESP_LOGI(TAG,"Websocket client %i was disconnected",num); |
|
268 break; |
|
269 |
|
270 case WEBSOCKET_DISCONNECT_ERROR: |
|
271 ESP_LOGI(TAG,"Websocket client %i was disconnected due to an error",num); |
|
272 break; |
|
273 |
|
274 case WEBSOCKET_TEXT: |
|
275 ESP_LOGI(TAG,"Websocket client %i sent text message of size %i:\n%s",num,(uint32_t)len,msg); |
|
276 dump_buf(msg, len); |
|
277 break; |
|
278 |
|
279 case WEBSOCKET_BIN: |
|
280 ESP_LOGI(TAG,"Websocket client %i sent bin message of size %i:\n",num,(uint32_t)len); |
|
281 dump_buf(msg, len); |
|
282 break; |
|
283 |
|
284 case WEBSOCKET_PING: |
|
285 ESP_LOGI(TAG,"client %i pinged us with message of size %i:\n%s",num,(uint32_t)len,msg); |
|
286 break; |
|
287 |
|
288 case WEBSOCKET_PONG: |
|
289 ESP_LOGI(TAG,"client %i responded to the ping",num); |
|
290 break; |
|
291 } |
|
292 } |
|
293 |
|
294 |
|
295 |
|
296 void dump_buf(char *buf, uint16_t buflen) |
|
297 { |
|
298 int i = 0, l = 1; |
|
299 |
|
300 printf("request length %d\n00: ", buflen); |
|
301 for (;;) { |
|
302 if (i >= buflen) |
|
303 break; |
|
304 if ((buf[i] < ' ') || (buf[i] > 126)) { |
|
305 if (buf[i] == '\n') { |
|
306 printf("\\n\n%02d: ", l); |
|
307 l++; |
|
308 } else if (buf[i] == '\r') { |
|
309 printf("\\r"); |
|
310 } else { |
|
311 printf("\\%02x", buf[i]); |
|
312 } |
|
313 } else { |
|
314 printf("%c", buf[i]); |
|
315 } |
|
316 i++; |
|
317 } |
|
318 printf("\n"); |
|
319 } |
|
320 |
|
321 |
|
322 |
|
323 /** |
|
324 * @brief Serve any client. |
|
325 * @param http_client_sock The socket on which the client is connected. |
|
326 */ |
|
327 static void http_serve(struct netconn *conn) |
|
328 { |
|
329 char ipstr[IPADDR_STRLEN_MAX]; |
|
330 struct netbuf *inbuf; |
|
331 static char *buf; |
|
332 static uint16_t buflen; |
|
333 static err_t err; |
|
334 char url[128], *p, *mod_since = NULL; |
|
335 ip_addr_t remote_addr; |
|
336 uint16_t remote_port; |
|
337 |
|
338 if (netconn_getaddr(conn, &remote_addr, &remote_port, 0) == ERR_OK) { |
|
339 strcpy(ipstr, ip4addr_ntoa(ip_2_ip4(&remote_addr))); |
|
340 } else { |
|
341 ipstr[0] = '\0'; |
|
342 } |
|
343 |
|
344 netconn_set_recvtimeout(conn,1000); // allow a connection timeout of 1 second |
|
345 err = netconn_recv(conn, &inbuf); |
|
346 |
|
347 if (err != ERR_OK) { |
|
348 if (err != ERR_TIMEOUT) { // Ignore timeout |
|
349 ESP_LOGI(TAG,"%s error %d on read", ipstr, err); |
|
350 } |
|
351 netconn_close(conn); |
|
352 netconn_delete(conn); |
|
353 netbuf_delete(inbuf); |
|
354 return; |
|
355 } |
|
356 |
|
357 netbuf_data(inbuf, (void**)&buf, &buflen); |
|
358 |
|
359 if (buf) { |
|
360 /* |
|
361 * Build url string from the data buffer. |
|
362 * It looks like: GET /app/localization.js HTTP/1.1 |
|
363 */ |
|
364 for (int i = 0; i < 10; i++) { |
|
365 if (buf[i] == ' ') { |
|
366 i++; |
|
367 for (int j = i; j < 128; j++) { |
|
368 url[j-i] = buf[j]; |
|
369 if (url[j-i] == ' ') { |
|
370 url[j-i] = '\0'; |
|
371 break; |
|
372 } |
|
373 } |
|
374 break; |
|
375 } |
|
376 } |
|
377 |
|
378 if (strstr(buf, "GET /logfiles.json")) { |
|
379 char temp[64]; |
|
380 FILE *dest = fopen("/spiffs/w/logfiles.json", "w"); |
|
381 if (dest) { |
|
382 fprintf(dest, "{\"Dir\":[{\"Folder\":\"/log\",\"Files\":["); |
|
383 DIR *dir = opendir("/sdcard/w/log"); |
|
384 if (dir) { |
|
385 struct dirent* de = readdir(dir); |
|
386 struct stat st; |
|
387 bool comma = false; |
|
388 while (de) { |
|
389 sprintf(temp, "/sdcard/w/log/%s", de->d_name); |
|
390 if (stat(temp, &st) == ESP_OK) { |
|
391 fprintf(dest, "%s{\"File\":\"%s\",\"Size\":%ld,\"Date\":%ld}", (comma)?",":"", de->d_name, st.st_size, st.st_mtime); |
|
392 comma = true; |
|
393 } |
|
394 de = readdir(dir); |
|
395 vTaskDelay(5 / portTICK_PERIOD_MS); |
|
396 } |
|
397 closedir(dir); |
|
398 } |
|
399 fprintf(dest, "]}]}"); |
|
400 fclose(dest); |
|
401 } |
|
402 http_sendfile(conn, "/logfiles.json", NULL, ipstr); |
|
403 netconn_close(conn); |
|
404 netconn_delete(conn); |
|
405 netbuf_delete(inbuf); |
|
406 unlink("/spiffs/w/logfiles.json"); |
|
407 return; |
|
408 } |
|
409 |
|
410 // http requests |
|
411 if (! strstr(buf,"Upgrade: websocket")) { // Not websocket |
|
412 p = strstr(buf, "If-Modified-Since:"); // May be cached |
|
413 if (p) { |
|
414 size_t mod_len = strcspn(p, " "); |
|
415 p += (int)(mod_len + 1); |
|
416 mod_len = strcspn(p, "\r\n"); |
|
417 mod_since = malloc(mod_len + 2); |
|
418 memcpy(mod_since, p, mod_len); |
|
419 mod_since[mod_len] = '\0'; |
|
420 } |
|
421 http_sendfile(conn, url, mod_since, ipstr); |
|
422 if (mod_since) |
|
423 free(mod_since); |
|
424 mod_since = NULL; |
|
425 netconn_close(conn); |
|
426 netconn_delete(conn); |
|
427 netbuf_delete(inbuf); |
|
428 return; |
|
429 } |
|
430 |
|
431 // websocket for noVNC. |
|
432 if ((strstr(buf,"GET /websockify ") && strstr(buf,"Upgrade: websocket"))) { |
|
433 int nr = ws_server_add_client_protocol(conn, buf, buflen, "/websockify", "binary", websockify_callback); |
|
434 ESP_LOGI(TAG, "%s new websocket on /websockify client: %d", ipstr, nr); |
|
435 netbuf_delete(inbuf); |
|
436 VncStartWS(nr); |
|
437 return; |
|
438 } |
|
439 |
|
440 // websocket for web UI. |
|
441 if ((strstr(buf,"GET /ws ") && strstr(buf,"Upgrade: websocket"))) { |
|
442 int nr = ws_server_add_client_protocol(conn, buf, buflen, "/ws", "binary", websock_callback); |
|
443 ESP_LOGI(TAG, "%s new websocket on /ws client: %d", ipstr, nr); |
|
444 netbuf_delete(inbuf); |
|
445 TFTstartWS(nr); |
|
446 // Startup something? Init webscreen? |
|
447 return; |
|
448 } |
|
449 |
|
450 dump_buf(buf, buflen); |
|
451 |
|
452 if (strstr(buf, "GET /")) { |
|
453 ESP_LOGI(TAG, "%s request: %s", ipstr, buf); |
|
454 http_error(conn, 404, "Not found", "Not found"); |
|
455 } else { |
|
456 http_error(conn, 405, "Invalid method", "Invalid method"); |
|
457 } |
|
458 } |
|
459 |
|
460 netconn_close(conn); |
|
461 netconn_delete(conn); |
|
462 netbuf_delete(inbuf); |
|
463 } |
|
464 |
|
465 |
|
466 |
|
467 /** |
|
468 * @brief Handles clients when they first connect. passes to a queue |
|
469 */ |
|
470 static void task_HTTPserver(void* pvParameters) |
|
471 { |
|
472 struct netconn *conn, *newconn; |
|
473 static err_t err; |
|
474 |
|
475 ESP_LOGI(TAG, "Starting http server_task"); |
|
476 client_queue = xQueueCreate(client_queue_size,sizeof(struct netconn*)); |
|
477 |
|
478 conn = netconn_new(NETCONN_TCP); |
|
479 netconn_bind(conn,NULL,80); |
|
480 netconn_listen(conn); |
|
481 |
|
482 do { |
|
483 err = netconn_accept(conn, &newconn); |
|
484 if (err == ERR_OK) { |
|
485 if (xQueueSendToBack(client_queue,&newconn,portMAX_DELAY) != pdTRUE) { |
|
486 ESP_LOGE(TAG, "xQueueSendToBack() queue full"); |
|
487 }; |
|
488 } |
|
489 vTaskDelay(5 / portTICK_PERIOD_MS); |
|
490 } while (err == ERR_OK); |
|
491 |
|
492 ESP_LOGE(TAG, "Stopping http server_task"); |
|
493 netconn_close(conn); |
|
494 netconn_delete(conn); |
|
495 vTaskDelete(NULL); |
|
496 } |
|
497 |
|
498 |
|
499 |
|
500 /** |
|
501 * @brief Receives clients from queue and handle them. |
|
502 */ |
|
503 static void task_Queue(void* pvParameters) |
|
504 { |
|
505 struct netconn* conn; |
|
506 |
|
507 ESP_LOGI(TAG, "Starting Queue task"); |
|
508 for(;;) { |
|
509 xQueueReceive(client_queue, &conn, portMAX_DELAY); |
|
510 if (!conn) |
|
511 continue; |
|
512 http_serve(conn); |
|
513 vTaskDelay(2 / portTICK_PERIOD_MS); |
|
514 } |
|
515 ESP_LOGE(TAG, "Stopping Queue task"); |
|
516 vTaskDelete(NULL); |
|
517 } |
|
518 |
|
519 |
|
520 |
|
521 void start_http_websocket(void) |
|
522 { |
|
523 ESP_LOGI(TAG, "Starting HTTP/Websocket server"); |
|
524 |
|
525 ws_server_start(); |
|
526 xTaskCreate(&task_HTTPserver, "HTTPserver", 3000, NULL, 9, &xTaskHTTP); |
|
527 xTaskCreate(&task_Queue, "Queue", 4000, NULL, 6, &xTaskQueue); |
|
528 } |
|
529 |