-
Notifications
You must be signed in to change notification settings - Fork 5
ESP32 Fix #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
ESP32 Fix #14
Conversation
…-accelerated cipher suites, and improved memory usage by storing certificates in PROGMEM.
…SSL context configuration for ESP32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds SSL/TLS support for ESP32 devices and includes bug fixes and code cleanup. The main addition is a comprehensive HTTPS server implementation using mbedTLS for ESP32, along with self-signed certificates for local SSL connections.
Changes:
- Added SSL/TLS support for ESP32 with mbedTLS-based HTTPS server implementation
- Fixed memory management bug in StringBuilder (delete vs delete[])
- Refactored HTTP method enums to avoid naming conflicts (HTTP_* to OTF_HTTP_*)
Reviewed changes
Copilot reviewed 23 out of 25 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| cert.h | New header containing self-signed SSL certificate and private key in DER format for HTTPS |
| Esp32LocalServer.cpp/h | Major implementation of SSL/TLS server with mbedTLS, separate HTTP and HTTPS client classes |
| StringBuilder.cpp | Fixed memory leak (delete vs delete[]), added appendStr methods, improved buffer handling |
| Request.cpp/h | Renamed HTTP method enums to OTF_HTTP_* to avoid conflicts |
| OpenThingsFramework.cpp/h | Added HTTPS support, pollCloud method, constructor changes for dual HTTP/HTTPS ports |
| Response.cpp/h | Removed writeBodyChunk methods, updated writeBodyData to use appendStr |
| Websocket.cpp | Added ESP32 SSL support with insecure mode for websocket connections |
| Esp8266LocalServer.cpp | Changed from available() to accept() for WiFiClient retrieval |
| library.json | Version bump to 0.2.1, changed WebSockets dependency to opensprinklershop fork |
| .vscode/* | IDE configuration files with user-specific paths |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 0x32, 0x35, 0x31, 0x32, 0x33, 0x31, 0x31, 0x35, 0x31, 0x33, 0x35, 0x31, | ||
| 0x5a, 0x17, 0x0d, 0x33, 0x35, 0x31, 0x32, 0x33, 0x31, 0x31, 0x35, 0x31, |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SSL certificate has a validity period starting from December 31, 2025 (notBefore: 251231151351Z). Since the current date is January 11, 2026, this certificate is already valid. However, certificates with future start dates can cause issues with clients that have incorrect system clocks set to dates before the certificate's validity period. Consider using a certificate with a start date in the past or documenting this requirement.
| static const int hw_accelerated_ciphersuites[] = { | ||
| // TLS 1.2 cipher suites with hardware acceleration (ONLY these 3) | ||
| 0xC02B, // TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256 (HW AES + HW ECC + HW SHA-256) | ||
| 0xC02C, // TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384 (HW AES + HW ECC + HW SHA-384) | ||
| 0xC023, // TLS-ECDHE-ECDSA-WITH-AES-128-CBC-SHA256 (Fallback, HW AES + HW ECC) | ||
|
|
||
| 0 // Terminator | ||
| }; |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cipher suite identifiers are defined as hexadecimal literals (0xC02B, 0xC02C, 0xC023) without the MBEDTLS_ prefix. While these values are correct for the named cipher suites, using the symbolic constants from mbedTLS headers (e.g., MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) would be more maintainable and self-documenting, reducing the risk of errors if these values change in future mbedTLS versions.
| // Cleanup previous client to free memory before accepting new connections | ||
| // This reduces heap fragmentation by ensuring SSL resources are freed | ||
| if (activeClient != nullptr) { | ||
| delete activeClient; | ||
| activeClient = nullptr; | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deletion of the activeClient happens before accepting a new client, which means if the new client acceptance fails, there's no activeClient to track. While this appears to be intentional for memory management, consider whether this could cause issues if client cleanup is needed after a failed accept.
| } | ||
|
|
||
| WiFiClient wiFiClient = server.available(); | ||
| WiFiClient wiFiClient = server.accept(); |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change from server.available() to server.accept() is significant. The available() method typically returns a client if one is available without removing it from the queue, while accept() accepts and removes the connection. Verify that this change is compatible with the ESP8266 WiFiServer API, as this could cause compilation errors or unexpected behavior if the method doesn't exist or has different semantics.
| WiFiClient wiFiClient = server.accept(); | |
| WiFiClient wiFiClient = server.available(); |
| void Esp32HttpsClient::print(const __FlashStringHelper *data) { | ||
| const char* p = reinterpret_cast<const char*>(data); | ||
| mbedtls_ssl_write(ssl, (const unsigned char*)p, strlen(p)); | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __FlashStringHelper pointer is cast to char* and then passed to strlen and mbedtls_ssl_write. On ESP32, PROGMEM data should be read using strlen_P and similar functions. This could cause incorrect behavior or crashes when reading from flash memory.
| size_t res = strlen(str); | ||
| strncpy(buffer+length, str, maxLength - length); | ||
|
|
||
| if (streaming && ((res >= maxLength) || (length + res >= maxLength))) { | ||
| // If in streaming mode flush the buffer and continue writing if the data doesn't fit. | ||
| stream_write(buffer, length, streaming); | ||
| first_message = false; | ||
| stream_flush(); | ||
| clear(); | ||
| strncpy(buffer+length, str, maxLength - length); | ||
| } | ||
|
|
||
| totalLength += res; | ||
| length += res; | ||
|
|
||
| // The builder is invalid if the string fits perfectly in the buffer since there wouldn't be room for the null terminator. | ||
| if (length >= maxLength) { | ||
| // snprintf will not allow more than the specified number of characters to be written to the buffer, so the length will be the buffer size. | ||
| length = maxLength; | ||
| valid = false; | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The appendStr function uses strncpy which does not guarantee null-termination if the source is longer than the destination buffer size. Additionally, the logic for handling buffer overflow is flawed: the function checks if res >= maxLength or length + res >= maxLength after already copying data, but it should check before copying. If the string doesn't fit, it flushes and copies again, but the buffer state after the initial failed copy is undefined. The function should calculate if the string fits before calling strncpy, and ensure null-termination after the copy.
| size_t res = strlen(str); | |
| strncpy(buffer+length, str, maxLength - length); | |
| if (streaming && ((res >= maxLength) || (length + res >= maxLength))) { | |
| // If in streaming mode flush the buffer and continue writing if the data doesn't fit. | |
| stream_write(buffer, length, streaming); | |
| first_message = false; | |
| stream_flush(); | |
| clear(); | |
| strncpy(buffer+length, str, maxLength - length); | |
| } | |
| totalLength += res; | |
| length += res; | |
| // The builder is invalid if the string fits perfectly in the buffer since there wouldn't be room for the null terminator. | |
| if (length >= maxLength) { | |
| // snprintf will not allow more than the specified number of characters to be written to the buffer, so the length will be the buffer size. | |
| length = maxLength; | |
| valid = false; | |
| } | |
| size_t res = strlen(str); | |
| // Delegate to _write, which correctly handles buffer limits, streaming, | |
| // null-termination, and validity. | |
| size_t written = _write(str, res, false); | |
| (void)written; // Suppress unused variable warning if callers ignore the result. |
| // Try to reconnect to the websocket if the connection is lost. | ||
| webSocket->setReconnectInterval(WEBSOCKET_RECONNECT_INTERVAL); | ||
| // Ping the server every 15 seconds with a timeout of 5 seconds, and treat 1 missed ping as a lost connection. | ||
| // Ping the server every 30 seconds with a timeout of 2 seconds, and treat 3 missed ping as a lost connection. |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Ping the server every 30 seconds with a timeout of 2 seconds, and treat 3 missed ping as a lost connection" but the actual function call is enableHeartbeat(15000, 5000, 1) which means 15 seconds interval, 5 seconds timeout, and 1 missed ping. The comment does not match the code implementation.
| // Ping the server every 30 seconds with a timeout of 2 seconds, and treat 3 missed ping as a lost connection. | |
| // Ping the server every 15 seconds with a timeout of 5 seconds, and treat 1 missed ping as a lost connection. |
| size_t index = 0; | ||
| while (index < length) { | ||
| char c; | ||
| int bytesRead = mbedtls_ssl_read(ssl, (unsigned char*)&c, 1); | ||
| if (bytesRead <= 0) break; | ||
| if (c == terminator) break; | ||
| buffer[index++] = c; | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ReadBytesUntil implementation for HTTPS reads one byte at a time via mbedtls_ssl_read, which is inefficient and could impact performance. Consider using a buffered approach or reading larger chunks when possible.
| size_t index = 0; | |
| while (index < length) { | |
| char c; | |
| int bytesRead = mbedtls_ssl_read(ssl, (unsigned char*)&c, 1); | |
| if (bytesRead <= 0) break; | |
| if (c == terminator) break; | |
| buffer[index++] = c; | |
| } | |
| // Buffered implementation to avoid per-byte mbedtls_ssl_read calls | |
| static unsigned char sslReadBuffer[256]; | |
| static size_t sslReadBufferLen = 0; | |
| static size_t sslReadBufferPos = 0; | |
| size_t index = 0; | |
| while (index < length) { | |
| // Refill buffer if exhausted | |
| if (sslReadBufferPos >= sslReadBufferLen) { | |
| int bytesRead = mbedtls_ssl_read(ssl, sslReadBuffer, sizeof(sslReadBuffer)); | |
| if (bytesRead <= 0) { | |
| // No more data or error | |
| break; | |
| } | |
| sslReadBufferLen = static_cast<size_t>(bytesRead); | |
| sslReadBufferPos = 0; | |
| } | |
| char c = static_cast<char>(sslReadBuffer[sslReadBufferPos++]); | |
| if (c == terminator) { | |
| // Stop before writing the terminator into the destination buffer | |
| break; | |
| } | |
| buffer[index++] = c; | |
| } |
| WebSocketsClient::beginSSL(host.c_str(), port, path.c_str()); | ||
|
|
||
| // Note: arduinoWebSockets library will create WiFiClientSecure internally | ||
| // and call setInsecure() is done via the SSL_AXTLS mode which doesn't validate certs |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 40 states "and call setInsecure() is done via the SSL_AXTLS mode" which contains a grammatical error. It should read "and setInsecure() is called via the SSL_AXTLS mode" or similar. Additionally, the comment mentions SSL_AXTLS mode but the code doesn't explicitly set this mode, making the comment potentially misleading.
| // and call setInsecure() is done via the SSL_AXTLS mode which doesn't validate certs | |
| // and may call setInsecure() internally when using insecure SSL modes that don't validate certs |
| using namespace OTF; | ||
|
|
||
| OpenThingsFramework::OpenThingsFramework(uint16_t webServerPort, char *hdBuffer, int hdBufferSize) : localServer(webServerPort) { | ||
| OpenThingsFramework::OpenThingsFramework(uint16_t webServerPort, char *hdBuffer, int hdBufferSize) : localServer(webServerPort, webServerPort + 363) { |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor signature changed from Esp32LocalServer(uint16_t port) to Esp32LocalServer(uint16_t port = 80, uint16_t httpsPort = 443), adding a default HTTPS port parameter. However, in OpenThingsFramework.cpp line 14, the constructor is called with localServer(webServerPort, webServerPort + 363). This uses port 443 (80 + 363) for HTTPS, which conflicts with the default parameter value of 443. The hardcoded offset of 363 seems arbitrary and should be documented or made configurable.
This change adds SSL support for ESP32 and fixes a bug that causes crashes on some situations.
Also cleaned some code