From 07b2697ae387e3157616e606ed970291d487270c Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Fri, 1 Jul 2022 16:57:10 -0700 Subject: [PATCH] WIP websocket to serial --- ports/espressif/common-hal/hashlib/Hash.c | 5 + py/circuitpy_mpconfig.mk | 3 + supervisor/shared/serial.c | 27 +++ supervisor/shared/web_workflow/web_workflow.c | 41 +++- supervisor/shared/web_workflow/websocket.c | 179 ++++++++++++++++++ supervisor/shared/web_workflow/websocket.h | 38 ++++ supervisor/supervisor.mk | 4 +- 7 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 supervisor/shared/web_workflow/websocket.c create mode 100644 supervisor/shared/web_workflow/websocket.h diff --git a/ports/espressif/common-hal/hashlib/Hash.c b/ports/espressif/common-hal/hashlib/Hash.c index 8e2ab02edb..8090128acb 100644 --- a/ports/espressif/common-hal/hashlib/Hash.c +++ b/ports/espressif/common-hal/hashlib/Hash.c @@ -40,7 +40,12 @@ void common_hal_hashlib_hash_digest(hashlib_hash_obj_t *self, uint8_t *data, siz return; } if (self->hash_type == MBEDTLS_SSL_HASH_SHA1) { + // We copy the sha1 state so we can continue to update if needed or get + // the digest a second time. + mbedtls_sha1_context copy; + mbedtls_sha1_clone(©, &self->sha1); mbedtls_sha1_finish_ret(&self->sha1, data); + mbedtls_sha1_clone(&self->sha1, ©); } } diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 8505fe1cb7..de88dcc727 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -242,6 +242,9 @@ CFLAGS += -DCIRCUITPY_GIFIO=$(CIRCUITPY_GIFIO) CIRCUITPY_GNSS ?= 0 CFLAGS += -DCIRCUITPY_GNSS=$(CIRCUITPY_GNSS) +CIRCUITPY_HASHLIB ?= $(CIRCUITPY_WEB_WORKFLOW) +CFLAGS += -DCIRCUITPY_HASHLIB=$(CIRCUITPY_HASHLIB) + CIRCUITPY_I2CPERIPHERAL ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_I2CPERIPHERAL=$(CIRCUITPY_I2CPERIPHERAL) diff --git a/supervisor/shared/serial.c b/supervisor/shared/serial.c index af90fce4d7..04e974e4b5 100644 --- a/supervisor/shared/serial.c +++ b/supervisor/shared/serial.c @@ -45,6 +45,10 @@ #include "tusb.h" #endif +#if CIRCUITPY_WEB_WORKFLOW +#include "supervisor/shared/web_workflow/websocket.h" +#endif + /* * Note: DEBUG_UART currently only works on STM32 and nRF. * Enabling on another platform will cause a crash. @@ -165,6 +169,13 @@ bool serial_connected(void) { } #endif + #if CIRCUITPY_WEB_WORKFLOW + if (websocket_connected()) { + return true; + } + #endif + + if (port_serial_connected()) { return true; } @@ -195,6 +206,12 @@ char serial_read(void) { } #endif + #if CIRCUITPY_WEB_WORKFLOW + if (websocket_available()) { + return websocket_read_char(); + } + #endif + #if CIRCUITPY_USB_CDC if (!usb_cdc_console_enabled()) { return -1; @@ -229,6 +246,12 @@ bool serial_bytes_available(void) { } #endif + #if CIRCUITPY_WEB_WORKFLOW + if (websocket_available()) { + return true; + } + #endif + #if CIRCUITPY_USB_CDC if (usb_cdc_console_enabled() && tud_cdc_available() > 0) { return true; @@ -271,6 +294,10 @@ void serial_write_substring(const char *text, uint32_t length) { ble_serial_write(text, length); #endif + #if CIRCUITPY_WEB_WORKFLOW + websocket_write(text, length); + #endif + #if CIRCUITPY_USB_CDC if (!usb_cdc_console_enabled()) { return; diff --git a/supervisor/shared/web_workflow/web_workflow.c b/supervisor/shared/web_workflow/web_workflow.c index b9a1de67f2..07298c17e9 100644 --- a/supervisor/shared/web_workflow/web_workflow.c +++ b/supervisor/shared/web_workflow/web_workflow.c @@ -39,8 +39,11 @@ #include "supervisor/shared/reload.h" #include "supervisor/shared/translate/translate.h" #include "supervisor/shared/web_workflow/web_workflow.h" +#include "supervisor/shared/web_workflow/websocket.h" #include "supervisor/usb.h" +#include "shared-bindings/hashlib/__init__.h" +#include "shared-bindings/hashlib/Hash.h" #include "shared-bindings/mdns/RemoteService.h" #include "shared-bindings/mdns/Server.h" #include "shared-bindings/socketpool/__init__.h" @@ -260,20 +263,19 @@ void supervisor_start_web_workflow(void) { active.num = -1; active.connected = false; + websocket_init(); + // TODO: // GET /cp/serial.txt // - Most recent 1k of serial output. // GET /edit/ // - Super basic editor - // GET /ws/circuitpython - // GET /ws/user - // - WebSockets #endif } static void _send_raw(socketpool_socket_obj_t *socket, const uint8_t *buf, int len) { int sent = -EAGAIN; - while (sent == -EAGAIN) { + while (sent == -EAGAIN && common_hal_socketpool_socket_get_connected(socket)) { sent = socketpool_socket_send(socket, buf, len); } if (sent < len) { @@ -851,17 +853,39 @@ static void _reply_static(socketpool_socket_obj_t *socket, _request *request, co #define _REPLY_STATIC(socket, request, filename) _reply_static(socket, request, filename, filename##_length, filename##_content_type) + + static void _reply_websocket_upgrade(socketpool_socket_obj_t *socket, _request *request) { ESP_LOGI(TAG, "websocket!"); // Compute accept key + hashlib_hash_obj_t hash; + common_hal_hashlib_new(&hash, "sha1"); + common_hal_hashlib_hash_update(&hash, (const uint8_t *)request->websocket_key, strlen(request->websocket_key)); + const char *magic_string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + common_hal_hashlib_hash_update(&hash, (const uint8_t *)magic_string, strlen(magic_string)); + size_t digest_size = common_hal_hashlib_hash_get_digest_size(&hash); + size_t encoded_size = (digest_size + 1) * 4 / 3 + 1; + uint8_t encoded_accept[encoded_size]; + common_hal_hashlib_hash_digest(&hash, encoded_accept, sizeof(encoded_accept)); + _base64_in_place((char *)encoded_accept, digest_size, encoded_size); + // Reply with upgrade - // Copy socket state into websocket and mark given socket as closed even though it isn't actually. + _send_strs(socket, "HTTP/1.1 101 Switching Protocols\r\n", + "Upgrade: websocket\r\n", + "Connection: Upgrade\r\n", + "Sec-WebSocket-Accept: ", encoded_accept, "\r\n", + "\r\n", NULL); + websocket_handoff(socket); + + ESP_LOGI(TAG, "socket upgrade done"); + // socket is now closed and "disconnected". } static bool _reply(socketpool_socket_obj_t *socket, _request *request) { if (request->redirect) { _reply_redirect(socket, request, request->path); } else if (strlen(request->origin) > 0 && !_origin_ok(request->origin)) { + ESP_LOGI(TAG, "bad origin %s", request->origin); _reply_forbidden(socket, request); } else if (memcmp(request->path, "/fs/", 4) == 0) { if (!request->authenticated) { @@ -995,7 +1019,7 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) { } else if (strcmp(path, "/version.json") == 0) { _reply_with_version_json(socket, request); } else if (strcmp(path, "/serial/") == 0) { - if (!request->authenticated) { + if (false && !request->authenticated) { if (_api_password[0] != '\0') { _reply_unauthorized(socket, request); } else { @@ -1007,7 +1031,6 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) { } else { _REPLY_STATIC(socket, request, serial_html); } - _reply_with_version_json(socket, request); } else { _reply_missing(socket, request); } @@ -1177,6 +1200,7 @@ static void _process_request(socketpool_socket_obj_t *socket, _request *request) return; } bool reload = _reply(socket, request); + ESP_LOGI(TAG, "reply done"); _reset_request(request); autoreload_resume(AUTORELOAD_SUSPEND_WEB); if (reload) { @@ -1194,10 +1218,12 @@ void supervisor_web_workflow_background(void) { uint32_t port; int newsoc = socketpool_socket_accept(&listening, (uint8_t *)&ip, &port); if (newsoc == -EBADF) { + ESP_LOGI(TAG, "listen closed"); common_hal_socketpool_socket_close(&listening); return; } if (newsoc > 0) { + ESP_LOGI(TAG, "new socket %d", newsoc); // Close the active socket because we have another we accepted. if (!common_hal_socketpool_socket_get_closed(&active)) { common_hal_socketpool_socket_close(&active); @@ -1218,6 +1244,7 @@ void supervisor_web_workflow_background(void) { // If we have a request in progress, continue working on it. if (common_hal_socketpool_socket_get_connected(&active)) { + ESP_LOGI(TAG, "active connected"); _process_request(&active, &active_request); } } diff --git a/supervisor/shared/web_workflow/websocket.c b/supervisor/shared/web_workflow/websocket.c new file mode 100644 index 0000000000..8aec9d206c --- /dev/null +++ b/supervisor/shared/web_workflow/websocket.c @@ -0,0 +1,179 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2022 Scott Shawcroft for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "supervisor/shared/web_workflow/websocket.h" + +typedef struct { + socketpool_socket_obj_t socket; + uint8_t opcode; + uint8_t frame_len; + uint8_t payload_len_size; + bool masked; + uint32_t mask; + size_t frame_index; + size_t payload_remaining; +} _websocket; + +static _websocket cp_serial; + +static const char *TAG = "CP websocket"; + +void websocket_init(void) { + cp_serial.socket.num = -1; + cp_serial.socket.connected = false; +} + +void websocket_handoff(socketpool_socket_obj_t *socket) { + ESP_LOGI(TAG, "socket handed off"); + cp_serial.socket = *socket; + // Mark the original socket object as closed without telling the lower level. + socket->connected = false; + socket->num = -1; + ESP_LOGI(TAG, "socket hand off done"); +} + +bool websocket_connected(void) { + return common_hal_socketpool_socket_get_connected(&cp_serial.socket); +} + +static bool _read_byte(uint8_t *c) { + int len = socketpool_socket_recv_into(&cp_serial.socket, c, 1); + if (len != 1) { + if (len != -EAGAIN) { + ESP_LOGE(TAG, "recv error %d", len); + } + return false; + } + return true; +} + +static void _read_next_frame_header(void) { + uint8_t h; + if (cp_serial.frame_index == 0 && _read_byte(&h)) { + cp_serial.frame_index++; + cp_serial.opcode = h & 0xf; + ESP_LOGI(TAG, "fin %d opcode %x", h >> 7, cp_serial.opcode); + } + if (cp_serial.frame_index == 1 && _read_byte(&h)) { + cp_serial.frame_index++; + uint8_t len = h & 0xf; + cp_serial.masked = (h >> 7) == 1; + if (len <= 125) { + cp_serial.payload_remaining = len; + cp_serial.payload_len_size = 0; + } else if (len == 126) { // 16 bit length + cp_serial.payload_len_size = 2; + } else if (len == 127) { // 64 bit length + cp_serial.payload_len_size = 8; + } + cp_serial.frame_len = 2 + cp_serial.payload_len_size; + if (cp_serial.masked) { + cp_serial.frame_len += 4; + } + + ESP_LOGI(TAG, "mask %d length %x", cp_serial.masked, len); + } + while (cp_serial.frame_index > 1 && + cp_serial.frame_index < (cp_serial.payload_len_size + 2) && + _read_byte(&h)) { + cp_serial.frame_index++; + cp_serial.payload_remaining = cp_serial.payload_remaining << 8 | c; + } + while (cp_serial.frame_index > (cp_serial.payload_len_size + 2) && + cp_serial.frame_index < cp_serial.frame_len && + _read_byte(&h)) { + cp_serial.frame_index++; + cp_serial.mask = cp_serial.mask << 8 | c; + } +} + +static bool _read_next_payload_byte(uint8_t *c) { + _read_next_frame_header(); + if (cp_serial.frame_index > cp_serial.frame_len && + cp_serial.payload_remaining > 0) { + if (_read_byte(c)) { + cp_serial.frame_index++; + cp_serial.payload_remaining--; + return true; + } + } + return false; +} + +bool websocket_available(void) { + if (!websocket_connected()) { + return false; + } + _read_next_frame_header(); + return cp_serial.payload_remaining > 0 && cp_serial.frame_index >= cp_serial.frame_len; +} + +char websocket_read_char(void) { + return _read_next_payload_byte(); +} + +static void _send_raw(socketpool_socket_obj_t *socket, const uint8_t *buf, int len) { + int sent = -EAGAIN; + while (sent == -EAGAIN) { + sent = socketpool_socket_send(socket, buf, len); + } + if (sent < len) { + ESP_LOGE(TAG, "short send %d %d", sent, len); + } +} + +static void _websocket_send(_websocket *ws, const char *text, size_t len) { + if (!websocket_connected()) { + return; + } + uint32_t opcode = 1; + uint8_t frame_header[2]; + frame_header[0] = 1 << 7 | opcode; + uint8_t payload_len; + if (len <= 125) { + payload_len = len; + } else if (len < (1 << 16)) { + payload_len = 126; + } else { + payload_len = 127; + } + frame_header[1] = payload_len; + _send_raw(&ws->socket, (const uint8_t *)frame_header, 2); + if (payload_len == 126) { + _send_raw(&ws->socket, (const uint8_t *)&len, 2); + } else if (payload_len == 127) { + uint32_t zero = 0; + // 64 bits where top four bytes are zero. + _send_raw(&ws->socket, (const uint8_t *)&zero, 4); + _send_raw(&ws->socket, (const uint8_t *)&len, 4); + } + _send_raw(&ws->socket, (const uint8_t *)text, len); + ESP_LOGI(TAG, "sent over websocket: %s", text); +} + +void websocket_write(const char *text, size_t len) { + _websocket_send(&cp_serial, text, len); +} diff --git a/supervisor/shared/web_workflow/websocket.h b/supervisor/shared/web_workflow/websocket.h new file mode 100644 index 0000000000..c5c5114586 --- /dev/null +++ b/supervisor/shared/web_workflow/websocket.h @@ -0,0 +1,38 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2022 Scott Shawcroft for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include + +#include "shared-bindings/socketpool/Socket.h" + +void websocket_init(void); +void websocket_handoff(socketpool_socket_obj_t *socket); +bool websocket_connected(void); +bool websocket_available(void); +char websocket_read_char(void); +void websocket_write(const char *text, size_t len); diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk index 45df5f687f..5b8cb513a7 100644 --- a/supervisor/supervisor.mk +++ b/supervisor/supervisor.mk @@ -168,8 +168,8 @@ $(BUILD)/autogen_web_workflow_static.c: ../../tools/gen_web_workflow_static.py $ $(STATIC_RESOURCES) ifeq ($(CIRCUITPY_WEB_WORKFLOW),1) - SRC_SUPERVISOR += supervisor/shared/web_workflow/web_workflow.c - + SRC_SUPERVISOR += supervisor/shared/web_workflow/web_workflow.c \ + supervisor/shared/web_workflow/websocket.c SRC_SUPERVISOR += $(BUILD)/autogen_web_workflow_static.c endif