From 157ce57996de890cee98b0dcf7eae3c82d10ef46 Mon Sep 17 00:00:00 2001 From: Jeroen88 Date: Fri, 15 May 2020 23:12:49 +0200 Subject: [PATCH 1/4] bugfix2/ESP8266HTTPClient (#6476) * Because of git problems, start from a new fork and create a new PR. This was PR #6457 * Style update to pass Travis * Update ReuseConnectionV2.ino * fix + enforce testing http code per @earlephilhower review * Close connection before ::connecting on HTTP/1.0 HTTPClient never actually closes the TCP connection on its own. It will leave the TCP connection open unless you explicitly do a getString which makes a StreamString and stuffs it with the HTTP server response, at which point the HTTP server itself will close the connection. If you check the HTTP error code and find failure, unless you do a getString and throw it away, it won't disconnect. Even in HTTP/1.0 or in cases when you haven't enabled _reuse. Change the logic in ::connect to only reuse the connection when it is specifically allowed. Otherwise, fall back to re-connection. * Adjust example per request Do single URL get in each loop, avoid infinite for loop at end. * Fix astyle * Clean up final pass notice * Fix example syntax error Editing code in a web textbox without running it is a painful process. Co-authored-by: Earle F. Philhower, III --- .../ReuseConnectionV2/ReuseConnectionV2.ino | 82 +++++++++++++++++++ .../src/ESP8266HTTPClient.cpp | 41 +++++----- 2 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 libraries/ESP8266HTTPClient/examples/ReuseConnectionV2/ReuseConnectionV2.ino diff --git a/libraries/ESP8266HTTPClient/examples/ReuseConnectionV2/ReuseConnectionV2.ino b/libraries/ESP8266HTTPClient/examples/ReuseConnectionV2/ReuseConnectionV2.ino new file mode 100644 index 000000000..42a89bece --- /dev/null +++ b/libraries/ESP8266HTTPClient/examples/ReuseConnectionV2/ReuseConnectionV2.ino @@ -0,0 +1,82 @@ +/** + reuseConnectionV2.ino + + Created on: 22.11.2015 + + This example reuses the http connection and also restores the connection if the connection is lost +*/ + + +#include +#include +#include + +#ifndef STASSID +#define STASSID "your-ssid" +#define STAPSK "your-password" +#endif + +ESP8266WiFiMulti WiFiMulti; + +HTTPClient http; +WiFiClient client; + +void setup() { + + Serial.begin(115200); + // Serial.setDebugOutput(true); + + Serial.println(); + Serial.println(); + Serial.println("Connecting to WiFi..."); + + WiFi.mode(WIFI_STA); + WiFiMulti.addAP(STASSID, STAPSK); + + // wait for WiFi connection + while ((WiFiMulti.run() != WL_CONNECTED)) { + Serial.write('.'); + delay(500); + } + Serial.println(" connected to WiFi"); + + // allow reuse (if server supports it) + http.setReuse(true); + + + http.begin(client, "http://jigsaw.w3.org/HTTP/connection.html"); + //http.begin(client, "jigsaw.w3.org", 80, "/HTTP/connection.html"); +} + +int pass = 0; + +void loop() { + // First 10 loop()s, retrieve the URL + if (pass < 10) { + pass++; + Serial.printf("Reuse connection example, GET url for the %d time\n", pass); + int httpCode = http.GET(); + if (httpCode > 0) { + Serial.printf("[HTTP] GET... code: %d\n", httpCode); + + // file found at server + if (httpCode == HTTP_CODE_OK) { + http.writeToStream(&Serial); + } + } else { + Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); + // Something went wrong with the connection, try to reconnect + http.end(); + http.begin(client, "http://jigsaw.w3.org/HTTP/connection.html"); + //http.begin(client, "jigsaw.w3.org", 80, "/HTTP/connection.html"); + } + + if (pass == 10) { + http.end(); + Serial.println("Done testing"); + } else { + Serial.println("\n\n\nWait 5 second...\n"); + delay(5000); + } + } +} diff --git a/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp b/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp index 5212dcf3e..85d27d5c6 100644 --- a/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp +++ b/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp @@ -457,6 +457,10 @@ void HTTPClient::disconnect(bool preserveClient) #endif } } else { + if (!preserveClient && _client) { // Also destroy _client if not connected() + _client = nullptr; + } + DEBUG_HTTPCLIENT("[HTTP-Client][end] tcp is closed\n"); } } @@ -970,7 +974,9 @@ int HTTPClient::writeToStream(Stream * stream) return returnError(HTTPC_ERROR_NO_STREAM); } - if(!connected()) { + // Only return error if not connected and no data available, because otherwise ::getString() will return an error instead of an empty + // string when the server returned a http code 204 (no content) + if(!connected() && _transferEncoding != HTTPC_TE_IDENTITY && _size > 0) { return returnError(HTTPC_ERROR_NOT_CONNECTED); } @@ -979,11 +985,13 @@ int HTTPClient::writeToStream(Stream * stream) int ret = 0; if(_transferEncoding == HTTPC_TE_IDENTITY) { - ret = writeToStreamDataBlock(stream, len); + if(len > 0) { + ret = writeToStreamDataBlock(stream, len); - // have we an error? - if(ret < 0) { - return returnError(ret); + // have we an error? + if(ret < 0) { + return returnError(ret); + } } } else if(_transferEncoding == HTTPC_TE_CHUNKED) { int size = 0; @@ -1198,12 +1206,8 @@ bool HTTPClient::hasHeader(const char* name) */ bool HTTPClient::connect(void) { - if(connected()) { - if(_reuse) { - DEBUG_HTTPCLIENT("[HTTP-Client] connect: already connected, reusing connection\n"); - } else { - DEBUG_HTTPCLIENT("[HTTP-Client] connect: already connected, try reuse!\n"); - } + if(_reuse && _canReuse && connected()) { + DEBUG_HTTPCLIENT("[HTTP-Client] connect: already connected, reusing connection\n"); while(_client->available() > 0) { _client->read(); } @@ -1334,6 +1338,7 @@ int HTTPClient::handleHeaderResponse() while(connected()) { size_t len = _client->available(); if(len > 0) { + int headerSeparator = -1; String headerLine = _client->readStringUntil('\n'); lastDataTime = millis(); @@ -1341,15 +1346,13 @@ int HTTPClient::handleHeaderResponse() DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] RX: '%s'\n", headerLine.c_str()); if (headerLine.startsWith(F("HTTP/1."))) { - if (_canReuse) { - _canReuse = (headerLine[sizeof "HTTP/1." - 1] != '0'); - } - _returnCode = headerLine.substring(9, headerLine.indexOf(' ', 9)).toInt(); - continue; - } - int headerSeparator = headerLine.indexOf(':'); - if (headerSeparator > 0) { + constexpr auto httpVersionIdx = sizeof "HTTP/1." - 1; + _canReuse = _canReuse && (headerLine[httpVersionIdx] != '0'); + _returnCode = headerLine.substring(httpVersionIdx + 2, headerLine.indexOf(' ', httpVersionIdx + 2)).toInt(); + _canReuse = _canReuse && (_returnCode > 0) && (_returnCode < 500); + + } else if ((headerSeparator = headerLine.indexOf(':')) > 0) { String headerName = headerLine.substring(0, headerSeparator); String headerValue = headerLine.substring(headerSeparator + 1); headerValue.trim(); From c5f60e31cd1afa839a823c9f376ce3a0fdc2144c Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Fri, 15 May 2020 20:21:50 -0700 Subject: [PATCH 2/4] Allocate BSSL stack for SigningVerifier (#7291) The BearSSL SigningVerifier was moved to the 2nd stack because some uses required much more stack than available on the normal stack. Add a reference to the second stack on object creation (which will allocate it, if there is no BSSL stack already allocated), and delete that reference on exit. Fixes #7288 --- libraries/ESP8266WiFi/src/BearSSLHelpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/ESP8266WiFi/src/BearSSLHelpers.h b/libraries/ESP8266WiFi/src/BearSSLHelpers.h index 282bab9cf..5c1b67546 100644 --- a/libraries/ESP8266WiFi/src/BearSSLHelpers.h +++ b/libraries/ESP8266WiFi/src/BearSSLHelpers.h @@ -24,6 +24,7 @@ #define _BEARSSLHELPERS_H #include +#include #include // Internal opaque structures, not needed by user applications @@ -157,7 +158,8 @@ class SigningVerifier : public UpdaterVerifyClass { virtual bool verify(UpdaterHashClass *hash, const void *signature, uint32_t signatureLen) override; public: - SigningVerifier(PublicKey *pubKey) { _pubKey = pubKey; } + SigningVerifier(PublicKey *pubKey) { _pubKey = pubKey; stack_thunk_add_ref(); } + ~SigningVerifier() { stack_thunk_del_ref(); } private: PublicKey *_pubKey; From 4519db85e981c6e505828baf94e8107a76db4f1a Mon Sep 17 00:00:00 2001 From: Ewald Comhaire Date: Sat, 16 May 2020 21:22:04 +0200 Subject: [PATCH 3/4] Server Sent Events example - issue #7008 (#7012) * Server Sent Events example - issue #7008 Illustrates the use of SSE using ESP8266WebServer * Update ServerSentEvents.ino * Create ServerSentEventsMultiClient.ino * sync * Update ServerSentEvents.ino * Update ServerSentEvents.ino Fix missing variables in printf statments Fix subscriptioncount not decreasing Fix SSEBroadcastState (argument sequence wrong) * Undo the library additions, move to current master * Fix compiler warning * Address review and fix multi-sensor updates Address points of @devyte's code review: * Use IPAddress vs. uint32_t * Refactor the URL parsing logic to use strlen vs. sizeof, since there was some confusion in the original (correct) version * Minimize copies of WiFiClients while in use * Use byref access for sensor updates Fix multi-sensor updates * Create an update Ticker for each sensor, because the original code only had one whose callback was overridden by sensorB, meaning sensorA never changed * Fix IPv6 build errors * Remove WiFiClient extraneous copy Avoid duplicating WiFiClient by using the WiFiClient object embedded in the subscriber[] array instead. Co-authored-by: Earle F. Philhower, III --- .../ServerSentEvents/ServerSentEvents.ino | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 libraries/ESP8266WebServer/examples/ServerSentEvents/ServerSentEvents.ino diff --git a/libraries/ESP8266WebServer/examples/ServerSentEvents/ServerSentEvents.ino b/libraries/ESP8266WebServer/examples/ServerSentEvents/ServerSentEvents.ino new file mode 100644 index 000000000..77ae1e958 --- /dev/null +++ b/libraries/ESP8266WebServer/examples/ServerSentEvents/ServerSentEvents.ino @@ -0,0 +1,223 @@ +/* Multi-client Server Sent Event (aka EventSource) demo + Run demo as follows: + 1. set SSID, password and ports, compile and run program + you should see (random) updates of sensors A and B + + 2. on the client(s), register it for the event bus using a REST API call: curl -sS "http://:/rest/events/subscribe" + on both server and client, you should now see that your client is registered + the server sends back the location of the event bus (channel) to the client: + subscription for client IP : event bus location: http://:/rest/events/ + + you will also see that the sensors are ready to broadcast state changes, but the client is not yet listening: + SSEBroadcastState - client > registered but not listening + + 3. on the client(s), start listening for events with: curl -sS "http://:/rest/events/" + if all is well, the following is being displayed on the ESP console + SSEHandler - registered client with IP is listening... + broadcast status change to client IP > for sensor[A|B] with new state > + every minute you will see on the ESP: SSEKeepAlive - client is still connected + + on the client, you should see the SSE messages coming in: + event: event + data: { "TYPE":"KEEP-ALIVE" } + event: event + data: { "TYPE":"STATE", "sensorB": {"state" : 12408, "prevState": 13502} } + event: event + data: { "TYPE":"STATE", "sensorA": {"state" : 17664, "prevState": 49362} } + + 4. on the client, stop listening by hitting control-C + on the ESP, after maximum one minute, the following message is displayed: SSEKeepAlive - client no longer connected, remove subscription + if you start listening again after the time expired, the "/rest/events" handle becomes stale and "Handle not found" is returned + you can also try to start listening again before the KeepAliver timer expires or simply register your client again +*/ + +extern "C" { +#include "c_types.h" +} +#include +#include +#include +#include +#include + +#ifndef STASSID +#define STASSID "your-ssid" +#define STAPSK "your-password" +#endif + +const char* ssid = STASSID; +const char* password = STAPSK; +const unsigned int port = 80; + +ESP8266WebServer server(port); + +#define SSE_MAX_CHANNELS 8 // in this simplified example, only eight SSE clients subscription allowed +struct SSESubscription { + IPAddress clientIP; + WiFiClient client; + Ticker keepAliveTimer; +} subscription[SSE_MAX_CHANNELS]; +uint8_t subscriptionCount = 0; + +typedef struct { + const char *name; + unsigned short value; + Ticker update; +} sensorType; +sensorType sensor[2]; + +void handleNotFound() { + Serial.println(F("Handle not found")); + String message = "Handle Not Found\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} + +void SSEKeepAlive() { + for (uint8_t i = 0; i < SSE_MAX_CHANNELS; i++) { + if (!(subscription[i].clientIP)) { + continue; + } + if (subscription[i].client.connected()) { + Serial.printf_P(PSTR("SSEKeepAlive - client is still listening on channel %d\n"), i); + subscription[i].client.println(F("event: event\ndata: { \"TYPE\":\"KEEP-ALIVE\" }\n")); // Extra newline required by SSE standard + } else { + Serial.printf_P(PSTR("SSEKeepAlive - client not listening on channel %d, remove subscription\n"), i); + subscription[i].keepAliveTimer.detach(); + subscription[i].client.flush(); + subscription[i].client.stop(); + subscription[i].clientIP = INADDR_NONE; + subscriptionCount--; + } + } +} + +// SSEHandler handles the client connection to the event bus (client event listener) +// every 60 seconds it sends a keep alive event via Ticker +void SSEHandler(uint8_t channel) { + WiFiClient client = server.client(); + SSESubscription &s = subscription[channel]; + if (s.clientIP != client.remoteIP()) { // IP addresses don't match, reject this client + Serial.printf_P(PSTR("SSEHandler - unregistered client with IP %s tries to listen\n"), server.client().remoteIP().toString().c_str()); + return handleNotFound(); + } + client.setNoDelay(true); + client.setSync(true); + Serial.printf_P(PSTR("SSEHandler - registered client with IP %s is listening\n"), IPAddress(s.clientIP).toString().c_str()); + s.client = client; // capture SSE server client connection + server.setContentLength(CONTENT_LENGTH_UNKNOWN); // the payload can go on forever + server.sendContent_P(PSTR("HTTP/1.1 200 OK\nContent-Type: text/event-stream;\nConnection: keep-alive\nCache-Control: no-cache\nAccess-Control-Allow-Origin: *\n\n")); + s.keepAliveTimer.attach_scheduled(30.0, SSEKeepAlive); // Refresh time every 30s for demo +} + +void handleAll() { + const char *uri = server.uri().c_str(); + const char *restEvents = PSTR("/rest/events/"); + if (strncmp_P(uri, restEvents, strlen_P(restEvents))) { + return handleNotFound(); + } + uri += strlen_P(restEvents); // Skip the "/rest/events/" and get to the channel number + unsigned int channel = atoi(uri); + if (channel < SSE_MAX_CHANNELS) { + return SSEHandler(channel); + } + handleNotFound(); +}; + +void SSEBroadcastState(const char *sensorName, unsigned short prevSensorValue, unsigned short sensorValue) { + for (uint8_t i = 0; i < SSE_MAX_CHANNELS; i++) { + if (!(subscription[i].clientIP)) { + continue; + } + String IPaddrstr = IPAddress(subscription[i].clientIP).toString(); + if (subscription[i].client.connected()) { + Serial.printf_P(PSTR("broadcast status change to client IP %s on channel %d for %s with new state %d\n"), + IPaddrstr.c_str(), i, sensorName, sensorValue); + subscription[i].client.printf_P(PSTR("event: event\ndata: {\"TYPE\":\"STATE\", \"%s\":{\"state\":%d, \"prevState\":%d}}\n\n"), + sensorName, sensorValue, prevSensorValue); + } else { + Serial.printf_P(PSTR("SSEBroadcastState - client %s registered on channel %d but not listening\n"), IPaddrstr.c_str(), i); + } + } +} + +// Simulate sensors +void updateSensor(sensorType &sensor) { + unsigned short newVal = (unsigned short)RANDOM_REG32; // (not so good) random value for the sensor + Serial.printf_P(PSTR("update sensor %s - previous state: %d, new state: %d\n"), sensor.name, sensor.value, newVal); + if (sensor.value != newVal) { + SSEBroadcastState(sensor.name, sensor.value, newVal); // only broadcast if state is different + } + sensor.value = newVal; + sensor.update.once(rand() % 20 + 10, std::bind(updateSensor, sensor)); // randomly update sensor +} + +void handleSubscribe() { + if (subscriptionCount == SSE_MAX_CHANNELS - 1) { + return handleNotFound(); // We ran out of channels + } + + uint8_t channel; + IPAddress clientIP = server.client().remoteIP(); // get IP address of client + String SSEurl = F("http://"); + SSEurl += WiFi.localIP().toString(); + SSEurl += F(":"); + SSEurl += port; + size_t offset = SSEurl.length(); + SSEurl += F("/rest/events/"); + + ++subscriptionCount; + for (channel = 0; channel < SSE_MAX_CHANNELS; channel++) // Find first free slot + if (!subscription[channel].clientIP) { + break; + } + subscription[channel] = {clientIP, server.client(), Ticker()}; + SSEurl += channel; + Serial.printf_P(PSTR("Allocated channel %d, on uri %s\n"), channel, SSEurl.substring(offset).c_str()); + //server.on(SSEurl.substring(offset), std::bind(SSEHandler, &(subscription[channel]))); + Serial.printf_P(PSTR("subscription for client IP %s: event bus location: %s\n"), clientIP.toString().c_str(), SSEurl.c_str()); + server.send_P(200, "text/plain", SSEurl.c_str()); +} + +void startServers() { + server.on(F("/rest/events/subscribe"), handleSubscribe); + server.onNotFound(handleAll); + server.begin(); + Serial.println("HTTP server and SSE EventSource started"); +} + +void setup(void) { + Serial.begin(115200); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + Serial.println(""); + while (WiFi.status() != WL_CONNECTED) { // Wait for connection + delay(500); + Serial.print("."); + } + Serial.printf_P(PSTR("\nConnected to %s with IP address: %s\n"), ssid, WiFi.localIP().toString().c_str()); + if (MDNS.begin("esp8266")) { + Serial.println("MDNS responder started"); + } + + startServers(); // start web and SSE servers + sensor[0].name = "sensorA"; + sensor[1].name = "sensorB"; + updateSensor(sensor[0]); + updateSensor(sensor[1]); +} + +void loop(void) { + server.handleClient(); + MDNS.update(); + yield(); +} From 1b20cd6263e2630ecd316699bc2fa853ca6835fb Mon Sep 17 00:00:00 2001 From: aerlon <39123666+aerlon@users.noreply.github.com> Date: Mon, 18 May 2020 19:54:25 +0200 Subject: [PATCH 4/4] - Add required BearSSL include in header. (#7310) Co-authored-by: Anders --- cores/esp8266/Crypto.cpp | 3 +-- cores/esp8266/Crypto.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cores/esp8266/Crypto.cpp b/cores/esp8266/Crypto.cpp index e51a29b8b..ae396b7d0 100644 --- a/cores/esp8266/Crypto.cpp +++ b/cores/esp8266/Crypto.cpp @@ -23,10 +23,9 @@ THE SOFTWARE. */ -#include #include "Crypto.h" #include - +#include #include namespace TypeCast = experimental::TypeConversion; diff --git a/cores/esp8266/Crypto.h b/cores/esp8266/Crypto.h index 435b4836a..790c916b6 100644 --- a/cores/esp8266/Crypto.h +++ b/cores/esp8266/Crypto.h @@ -27,6 +27,7 @@ #define __ESP8266_ARDUINO_CRYPTO_H__ #include +#include namespace experimental { @@ -745,8 +746,7 @@ struct HKDF private: - // Use an opaque type to avoid #include which drags the lib declarations into userland. The global scope prefix is required for compilation to succeed, it seems. - ::br_hkdf_context hkdfContext; + br_hkdf_context hkdfContext; };