Update to version 3.10.0

This commit is contained in:
2026-02-15 11:52:28 +01:00
parent c98a476228
commit f5e5d92d31
45 changed files with 5602 additions and 253 deletions

View File

@@ -0,0 +1,225 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Mitch Bradley
//
// - Test for chunked encoding in requests
//
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
using namespace asyncsrv;
// Tests:
//
// Upload a file with PUT
// curl -T myfile.txt http://192.168.4.1/
//
// Upload a file with PUT using chunked encoding
// curl -T bigfile.txt -H 'Transfer-Encoding: chunked' http://192.168.4.1/
// ** Note: If the file will not fit in the available space, the server
// ** does not know that in advance due to the lack of a Content-Length header.
// ** The transfer will proceed until the filesystem fills up, then the transfer
// ** will fail and the partial file will be deleted. This works correctly with
// ** recent versions (e.g. pioarduino) of the arduinoespressif32 framework, but
// ** fails with the stale 3.20017.241212+sha.dcc1105b version due to a LittleFS
// ** bug that has since been fixed.
//
// Immediately reject a chunked PUT that will not fit in available space
// curl -T bigfile.txt -H 'Transfer-Encoding: chunked' -H 'X-Expected-Entity-Length: 99999999' http://192.168.4.1/
// ** Note: MacOS WebDAVFS supplies the X-Expected-Entity-Length header with its
// ** chunked PUTs
// Malformed chunk (triggers abort)
// printf 'PUT /bad HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\n12345\r\nZ\r\n' | nc 192.168.4.1 80
// This struct is used with _tempObject to communicate between handleBody and a subsequent handleRequest
struct RequestState {
File outFile;
};
void handleRequest(AsyncWebServerRequest *request) {
Serial.print(request->methodToString());
Serial.print(" ");
Serial.println(request->url());
if (request->method() != HTTP_PUT) {
request->send(400); // Bad Request
return;
}
// If request->_tempObject is not null, handleBody already
// did the necessary work for a PUT operation. Otherwise,
// handleBody was either not called, or did nothing, so we
// handle the request later in this routine. That happens
// when a non-chunked PUT has Content-Length: 0.
auto state = static_cast<RequestState *>(request->_tempObject);
if (state) {
// If handleBody successfully opened the file, whether or not it
// wrote data to it, we close it here and send the "created"
// response. If handleBody did not open the file, because the
// open attempt failed or because the operation was rejected,
// state will be non-null but state->outFile will be false. In
// that case, handleBody has already sent an appropriate
// response code.
if (state->outFile) {
// The file was already opened and written in handleBody so
// we close it here and issue the appropriate response.
state->outFile.close();
request->send(201); // Created
}
// The resources used by state will be automatically freed
// when the framework frees the _tempObject pointer
return;
}
String path = request->url();
// This PUT code executes if the body was empty, which
// can happen if the client creates a zero-length file.
// MacOS WebDAVFS does that, then later LOCKs the file
// and issues a subsequent PUT with body contents.
#ifdef ESP32
File file = LittleFS.open(path, "w", true);
#else
File file = LittleFS.open(path, "w");
#endif
if (file) {
file.close();
request->send(201); // Created
return;
}
request->send(403);
}
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (request->method() == HTTP_PUT) {
auto state = static_cast<RequestState *>(request->_tempObject);
if (index == 0) {
// parse the url to a proper path
String path = request->url();
// Allocate the _tempObject memory
request->_tempObject = std::malloc(sizeof(RequestState));
// Use placement new to construct the RequestState object therein
state = new (request->_tempObject) RequestState{File()};
// If the client disconnects or there is a parsing error,
// handleRequest will not be called so we need to close
// the file. The memory backing _tempObject will be freed
// automatically.
request->onDisconnect([request]() {
Serial.println("Client disconnected");
auto state = static_cast<RequestState *>(request->_tempObject);
if (state) {
if (state->outFile) {
state->outFile.close();
}
}
});
if (total) {
#ifdef ESP32
size_t avail = LittleFS.totalBytes() - LittleFS.usedBytes();
#else
FSInfo info;
LittleFS.info(info);
auto avail = info.totalBytes - info.usedBytes;
#endif
avail = (avail >= 4096) ? avail - 4096 : avail; // Reserve a block for overhead
if (total > avail) {
Serial.printf("PUT %zu bytes will not fit in available space (%zu).\n", total, avail);
request->send(507, "text/plain", "Too large for available storage\r\n");
return;
}
}
Serial.print("Opening ");
Serial.print(path);
Serial.println(" from handleBody");
#ifdef ESP32
File file = LittleFS.open(path, "w", true);
#else
File file = LittleFS.open(path, "w");
#endif
if (!file) {
request->send(500, "text/plain", "Cannot create the file");
return;
}
if (file.isDirectory()) {
file.close();
Serial.println("Cannot PUT to a directory");
request->send(403, "text/plain", "Cannot PUT to a directory");
return;
}
// If we already returned, the File object in
// request->_tempObject is the default-constructed one. The
// presence of a non-default-constructed File in state->outFile
// indicates that the file was opened successfully and is ready
// to receive body data. The File will be closed later when
// handleRequest is called after all calls to handleBody
std::swap(state->outFile, file);
// Now request->_tempObject contains the actual file object which owns it,
// and default-constructed File() object is in file, which will
// go out of scope
}
if (state && state->outFile) {
Serial.printf("Writing %zu bytes at offset %zu\n", len, index);
auto actual = state->outFile.write(data, len);
if (actual != len) {
Serial.println("WebDAV write failed. Deleting file.");
// Replace the File object in state with a null one
File file{};
std::swap(state->outFile, file);
file.close();
String path = request->url();
LittleFS.remove(path);
request->send(507, "text/plain", "Too large for available storage\r\n");
return;
}
}
}
}
static AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
#ifdef ESP32
LittleFS.begin(true);
#else
LittleFS.begin();
#endif
server.onRequestBody(handleBody);
server.onNotFound(handleRequest);
server.begin();
}
void loop() {
delay(100);
}

View File

@@ -0,0 +1,65 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Mitch Bradley
//
// - Test for additional WebDAV request methods
//
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
using namespace asyncsrv;
// Tests:
//
// Send requests with various methods
// curl -s -X PROPFIND http://192.168.4.1/
// curl -s -X LOCK http://192.168.4.1/
// curl -s -X UNLOCK http://192.168.4.1/
// curl -s -X PROPPATCH http://192.168.4.1/
// curl -s -X MKCOL http://192.168.4.1/
// curl -s -X MOVE http://192.168.4.1/
// curl -s -X COPY http://192.168.4.1/
//
// In all cases, the request will be accepted with text/plain response 200 like
// "Got method PROPFIND on URL /"
static AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
server.onNotFound([](AsyncWebServerRequest *request) {
String resp("Got method ");
resp += request->methodToString();
resp += " on URL ";
resp += request->url();
resp += "\r\n";
Serial.print(resp);
request->send(200, "text/plain", resp.c_str());
});
server.begin();
}
void loop() {
delay(100);
}

View File

@@ -49,6 +49,11 @@ static const char *htmlContent PROGMEM = R"(
ws.send(message);
console.log("WebSocket sent: " + message);
}
setInterval(function() {
if (ws.readyState === WebSocket.OPEN) {
ws.send("msg from browser");
}
}, 1000);
</script>
</body>
</html>
@@ -76,9 +81,11 @@ void setup() {
// Run in terminal 2: websocat ws://192.168.4.1/ws => should stream data
// Run in terminal 3: websocat ws://192.168.4.1/ws => should fail:
//
// To send a message to the WebSocket server:
// To send a message to the WebSocket server (\n at the end):
// > echo "Hello!" | websocat ws://192.168.4.1/ws
//
// echo "Hello!" | websocat ws://192.168.4.1/ws
// Generates 2001 characters (\n at the end) to cause a fragmentation (over TCP MSS):
// > openssl rand -hex 1000 | websocat ws://192.168.4.1/ws
//
ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
(void)len;
@@ -101,14 +108,50 @@ void setup() {
} else if (type == WS_EVT_DATA) {
AwsFrameInfo *info = (AwsFrameInfo *)arg;
Serial.printf("index: %" PRIu64 ", len: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 "\n", info->index, info->len, info->final, info->opcode);
String msg = "";
Serial.printf(
"index: %" PRIu64 ", len: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", framelen: %d\n", info->index, info->len, info->final,
info->message_opcode, len
);
// complete frame
if (info->final && info->index == 0 && info->len == len) {
if (info->opcode == WS_TEXT) {
data[len] = 0;
if (info->message_opcode == WS_TEXT) {
Serial.printf("ws text: %s\n", (char *)data);
client->ping();
}
} else {
// incomplete frame
if (info->index == 0) {
if (info->num == 0) {
Serial.printf(
"ws[%s][%" PRIu32 "] [%" PRIu32 "] MSG START %s\n", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT) ? "text" : "binary"
);
}
Serial.printf("ws[%s][%" PRIu32 "] [%" PRIu32 "] FRAME START len=%" PRIu64 "\n", server->url(), client->id(), info->num, info->len);
}
Serial.printf(
"ws[%s][%" PRIu32 "] [%" PRIu32 "] FRAME %s, index=%" PRIu64 ", len=%" PRIu32 "]: ", server->url(), client->id(), info->num,
(info->message_opcode == WS_TEXT) ? "text" : "binary", info->index, (uint32_t)len
);
if (info->message_opcode == WS_TEXT) {
Serial.printf("%s\n", (char *)data);
} else {
for (size_t i = 0; i < len; i++) {
Serial.printf("%02x ", data[i]);
}
Serial.printf("\n");
}
if ((info->index + len) == info->len) {
Serial.printf("ws[%s][%" PRIu32 "] [%" PRIu32 "] FRAME END\n", server->url(), client->id(), info->num);
if (info->final) {
Serial.printf("ws[%s][%" PRIu32 "] [%" PRIu32 "] MSG END\n", server->url(), client->id(), info->num);
}
}
}
}
});
@@ -130,25 +173,40 @@ void setup() {
server.begin();
}
static uint32_t lastWS = 0;
static uint32_t deltaWS = 100;
#ifdef ESP32
static const uint32_t deltaWS = 50;
#else
static const uint32_t deltaWS = 200;
#endif
static uint32_t lastWS = 0;
static uint32_t lastHeap = 0;
void loop() {
uint32_t now = millis();
if (now - lastWS >= deltaWS) {
ws.printfAll("kp%.4f", (10.0 / 3.0));
ws.printfAll("kp:%.4f", (10.0 / 3.0));
lastWS = millis();
}
if (now - lastHeap >= 2000) {
if (now - lastHeap >= 5000) {
Serial.printf("Connected clients: %u / %u total\n", ws.count(), ws.getClients().size());
// this can be called to also set a soft limit on the number of connected clients
ws.cleanupClients(2); // no more than 2 clients
String random;
random.reserve(8192);
for (size_t i = 0; i < 8192; i++) {
random += "x";
}
ws.textAll(random);
// ping twice (2 control frames)
ws.pingAll();
ws.pingAll();
#ifdef ESP32
Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
#endif