From 2617252395acc0419e4f28b204cebe334645a129 Mon Sep 17 00:00:00 2001 From: Pablo2048 Date: Mon, 18 Aug 2025 11:51:22 +0200 Subject: [PATCH] Update to version 3.8.0 --- .clang-format | 246 ++++++++++++++++++ .codespellrc | 8 + .editorconfig | 60 +++++ .gitignore | 5 + .gitpod.Dockerfile | 2 + .gitpod.yml | 9 + .pre-commit-config.yaml | 42 +++ README.md | 13 + data/README.md | 48 ---- .../AsyncResponseStream.ino | 4 +- examples/AsyncTunnel/AsyncTunnel.ino | 210 +++++++++++++++ examples/Auth/Auth.ino | 4 +- examples/CORS/CORS.ino | 4 +- examples/CaptivePortal/CaptivePortal.ino | 6 +- examples/CatchAllHandler/CatchAllHandler.ino | 4 +- examples/ChunkResponse/ChunkResponse.ino | 4 +- .../ChunkRetryResponse/ChunkRetryResponse.ino | 21 +- examples/EndBegin/EndBegin.ino | 4 +- examples/Filters/Filters.ino | 18 +- examples/FlashResponse/FlashResponse.ino | 4 +- .../HeaderManipulation/HeaderManipulation.ino | 27 +- examples/Headers/Headers.ino | 4 +- examples/Json/Json.ino | 17 +- examples/Logging/Logging.ino | 4 +- examples/MessagePack/MessagePack.ino | 4 +- examples/Middleware/Middleware.ino | 4 +- examples/Params/Params.ino | 4 +- .../PartitionDownloader.ino | 4 +- examples/PerfTests/PerfTests.ino | 4 +- examples/RateLimit/RateLimit.ino | 4 +- examples/Redirect/Redirect.ino | 4 +- .../RequestContinuation.ino | 4 +- .../RequestContinuationComplete.ino | 4 +- .../ResumableDownload/ResumableDownload.ino | 4 +- examples/Rewrite/Rewrite.ino | 4 +- .../ServerSentEvents/ServerSentEvents.ino | 4 +- .../ServerSentEvents_PR156.ino | 141 ++++++++++ examples/ServerState/ServerState.ino | 4 +- .../SkipServerMiddleware.ino | 4 +- .../SlowChunkResponse/SlowChunkResponse.ino | 4 +- examples/StaticFile/StaticFile.ino | 38 ++- examples/Templates/Templates.ino | 4 +- examples/Upload/Upload.ino | 4 +- examples/WebSocket/WebSocket.ino | 10 +- examples/WebSocketEasy/WebSocketEasy.ino | 4 +- idf_component.yml | 7 +- idf_component_examples/catchall/main/main.cpp | 2 +- .../serversentevents/main/main.cpp | 2 +- .../websocket/main/main.cpp | 2 +- library.json => library.json_ | 12 +- library.properties | 2 +- .../IncreaseMaxSockets/platformio.ini | 4 +- platformio.ini | 50 +++- src/AsyncEventSource.cpp | 27 +- src/AsyncEventSource.h | 11 +- src/AsyncJson.cpp | 74 ++++-- src/AsyncJson.h | 7 +- src/AsyncWebHeader.cpp | 48 ++-- src/AsyncWebServerRequest.cpp | 85 ++++++ src/AsyncWebServerVersion.h | 4 +- src/AsyncWebSocket.cpp | 118 ++++----- src/AsyncWebSocket.h | 68 ++++- src/ESPAsyncWebServer.h | 41 ++- src/Middleware.cpp | 4 + src/WebHandlers.cpp | 5 +- src/WebRequest.cpp | 22 +- src/WebResponseImpl.h | 1 - src/WebResponses.cpp | 203 ++++++++++----- src/WebServer.cpp | 12 +- src/literals.h | 142 +++++----- 70 files changed, 1541 insertions(+), 441 deletions(-) create mode 100644 .clang-format create mode 100644 .codespellrc create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitpod.Dockerfile create mode 100644 .gitpod.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 data/README.md create mode 100644 examples/AsyncTunnel/AsyncTunnel.ino create mode 100644 examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino rename library.json => library.json_ (88%) create mode 100644 src/AsyncWebServerRequest.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8f47348 --- /dev/null +++ b/.clang-format @@ -0,0 +1,246 @@ +# Clang format version: 18.1.3 +--- +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveShortCaseStatements: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCaseColons: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowBreakBeforeNoexceptSpecifier: Never +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: true + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakAdjacentStringLiterals: true +BreakAfterAttributes: Always +BreakAfterJavaFieldAnnotations: false +BreakArrays: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: Always +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: 160 +CommentPragmas: "" +CompactNamespaces: false +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: ^"(llvm|llvm-c|clang|clang-c)/ + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: ^(<|"(gtest|gmock|isl|json)/) + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: .* + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: "" +IncludeIsMainSourceRegex: "" +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: NoIndent +IndentGotoLabels: false +IndentPPDirectives: None +IndentRequiresClause: false +IndentWidth: 2 +IndentWrappedFunctionNames: true +InsertBraces: true +InsertNewlineAtEOF: true +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtEOF: false +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +Language: Cpp +LineEnding: LF +MacroBlockBegin: "" +MacroBlockEnd: "" +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PPIndentWidth: -1 +PackConstructorInitializers: BinPack +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakScopeResolution: 500 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +QualifierAlignment: Leave +ReferenceAlignment: Pointer +ReflowComments: false +RemoveBracesLLVM: false +RemoveParentheses: Leave +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SkipMacroDefinitionBody: false +SortIncludes: Never +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeJsonColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDeclarationName: false + AfterFunctionDefinitionName: false + AfterIfMacros: true + AfterOverloadedOperator: true + AfterPlacementOperator: true + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInContainerLiterals: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Never +SpacesInParensOptions: + InConditionalStatements: false + InCStyleCasts: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 2 +UseTab: Never +VerilogBreakBetweenInstancePorts: true +WhitespaceSensitiveMacros: + - BOOST_PP_STRINGIZE + - CF_SWIFT_NAME + - NS_SWIFT_NAME + - PP_STRINGIZE + - STRINGIZE +BracedInitializerIndentWidth: 2 diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..d26ee41 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,8 @@ +[codespell] +# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/spell-check/.codespellrc +# In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: +ignore-words-list = ba,licence,varius +skip = ./.git,./.licenses,__pycache__,.clang-format,.codespellrc,.editorconfig,.flake8,.prettierignore,.yamllint.yml,.gitignore +builtin = clear,informal,en-GB_to_en-US +check-filenames = +check-hidden = diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e22936c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,60 @@ +# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/general/.editorconfig +# See: https://editorconfig.org/ +# The formatting style defined in this file is the official standardized style to be used in all Arduino Tooling +# projects and should not be modified. +# Note: indent style for each file type is defined even when it matches the universal config in order to make it clear +# that this type has an official style. + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{adoc,asc,asciidoc}] +indent_size = 2 +indent_style = space + +[*.{bash,sh}] +indent_size = 4 +indent_style = space + +[*.{c,cc,cp,cpp,cxx,h,hh,hpp,hxx,ii,inl,ino,ixx,pde,tpl,tpp,txx}] +indent_size = 2 +indent_style = space + +[*.{go,mod}] +indent_style = tab + +[*.java] +indent_size = 2 +indent_style = space + +[*.{js,jsx,json,jsonc,json5,ts,tsx}] +indent_size = 2 +indent_style = space + +[*.{md,mdx,mkdn,mdown,markdown}] +indent_size = unset +indent_style = space + +[*.proto] +indent_size = 2 +indent_style = space + +[*.py] +indent_size = 4 +indent_style = space + +[*.svg] +indent_size = 2 +indent_style = space + +[*.{yaml,yml}] +indent_size = 2 +indent_style = space + +[{.gitconfig,.gitmodules}] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1efbc8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.lh +/.pio +/.vscode +/logs diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 0000000..29eeb43 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,2 @@ +FROM gitpod/workspace-python-3.11 +USER gitpod diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..2f8a443 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,9 @@ +tasks: + - command: pip install --upgrade pip && pip install -U platformio && platformio run + +image: + file: .gitpod.Dockerfile + +vscode: + extensions: + - shardulm94.trailing-spaces diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..eb2d62e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +exclude: | + (?x)( + ^\.github\/| + LICENSE$ + ) + +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v5.0.0" + hooks: + # Generic checks + - id: check-case-conflict + - id: check-symlinks + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + exclude: ^.*\.(bin|BIN)$ + - id: mixed-line-ending + args: [--fix=lf] + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: ^platformio\.ini$ + + - repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + # Spell checking + - id: codespell + exclude: ^.*\.(svd|SVD)$ + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: "v18.1.3" + hooks: + # C/C++ formatting + - id: clang-format + types_or: [c, c++] + exclude: ^.*\/build_opt\.h$ diff --git a/README.md b/README.md index 9bb7575..1db7aee 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,19 @@ lib_deps = ESP32Async/ESPAsyncWebServer ``` +### LibreTiny (BK7231N/T, RTL8710B, etc.) + +Version 1.9.1 or newer is required. + +```ini +[env:stable] +platform = libretiny @ ^1.9.1 +lib_ldf_mode = chain +lib_deps = + ESP32Async/AsyncTCP + ESP32Async/ESPAsyncWebServer +``` + ### Unofficial dependencies **AsyncTCPSock** diff --git a/data/README.md b/data/README.md deleted file mode 100644 index 96a2ee4..0000000 --- a/data/README.md +++ /dev/null @@ -1,48 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod -rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper -arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit -accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. -Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo -dapibus elit, id varius sem dui id lacus. diff --git a/examples/AsyncResponseStream/AsyncResponseStream.ino b/examples/AsyncResponseStream/AsyncResponseStream.ino index 62fa799..451bb1a 100644 --- a/examples/AsyncResponseStream/AsyncResponseStream.ino +++ b/examples/AsyncResponseStream/AsyncResponseStream.ino @@ -2,7 +2,7 @@ // Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -20,7 +20,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/AsyncTunnel/AsyncTunnel.ino b/examples/AsyncTunnel/AsyncTunnel.ino new file mode 100644 index 0000000..b63c056 --- /dev/null +++ b/examples/AsyncTunnel/AsyncTunnel.ino @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// Shows how to trigger an async client request from a browser request and send the client response back to the browser through websocket +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +#define WIFI_SSID "IoT" +#define WIFI_PASSWORD "" + +static AsyncWebServer server(80); +static AsyncWebSocketMessageHandler wsHandler; +static AsyncWebSocket ws("/ws", wsHandler.eventHandler()); + +static const char *htmlContent PROGMEM = R"( + + + + WebSocket Tunnel Example + + +

WebSocket Tunnel Example

+
+
+
+ + + + )"; +static const size_t htmlContentLength = strlen_P(htmlContent); + +void setup() { + Serial.begin(115200); + +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + } + Serial.println("Connected to WiFi!"); + Serial.println(WiFi.localIP()); +#endif + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength); + }); + + wsHandler.onMessage([](AsyncWebSocket *server, AsyncWebSocketClient *wsClient, const uint8_t *data, size_t len) { + String url; + String host; + String port; + String path; + + url.concat((const char *)data, len); + + if (!url.startsWith("http://")) { + return; + } + + if (!url.endsWith("/")) { + url += "/"; + } + + // Parse the URL to extract the host and port + int start = url.indexOf("://") + 3; + int end = url.indexOf("/", start); + if (end == -1) { + end = url.length(); + } + String hostPort = url.substring(start, end); + int colonIndex = hostPort.indexOf(":"); + if (colonIndex != -1) { + host = hostPort.substring(0, colonIndex); + port = hostPort.substring(colonIndex + 1); + } else { + host = hostPort; + port = "80"; // Default HTTP port + } + path = url.substring(end); + + Serial.printf("Host: %s\n", host.c_str()); + Serial.printf("Port: %s\n", port.c_str()); + Serial.printf("Path: %s\n", path.c_str()); + + // Ensure client does not get deleted while the websocket holds a reference to it + std::shared_ptr *safeAsyncClient = new std::shared_ptr(std::make_shared()); + AsyncClient *asyncClient = safeAsyncClient->get(); + + asyncClient->onDisconnect([safeAsyncClient](void *arg, AsyncClient *client) { + Serial.printf("Tunnel disconnected!\n"); + delete safeAsyncClient; + }); + + // register a callback when an error occurs + // note: onDisconnect also called on error + asyncClient->onError([](void *arg, AsyncClient *client, int8_t error) { + Serial.printf("Tunnel error: %s\n", client->errorToString(error)); + }); + + // register a callback when data arrives, to accumulate it + asyncClient->onPacket( + [safeAsyncClient](void *arg, AsyncClient *, struct pbuf *pb) { + std::shared_ptr safeAsyncClientRef = *safeAsyncClient; // add a reference + AsyncWebSocketClient *wsClient = (AsyncWebSocketClient *)arg; + Serial.printf("Tunnel received %u bytes\n", pb->len); + AsyncWebSocketSharedBuffer wsBuffer = + AsyncWebSocketSharedBuffer(new std::vector((uint8_t *)pb->payload, (uint8_t *)pb->payload + pb->len), [=](std::vector *bufptr) { + delete bufptr; + Serial.printf("ACK %u bytes\n", pb->len); + safeAsyncClientRef->ackPacket(pb); + }); + Serial.printf("Tunnel sending %u bytes\n", wsBuffer->size()); + Serial.printf("%.*s\n", (int)wsBuffer->size(), wsBuffer->data()); + wsClient->binary(std::move(wsBuffer)); + }, + wsClient + ); + + asyncClient->onConnect([=](void *arg, AsyncClient *client) { + Serial.printf("Tunnel connected!\n"); + + client->write("GET "); + client->write(path.c_str()); + client->write(" HTTP/1.1\r\n"); + client->write("Host: "); + client->write(host.c_str()); + client->write(":"); + client->write(port.c_str()); + client->write("\r\n"); + client->write("User-Agent: ESP32\r\n"); + client->write("Accept: */*\r\n"); + client->write("Connection: close\r\n"); + client->write("\r\n"); + }); + + Serial.printf("Fetching: http://%s:%s%s\n", host.c_str(), port.c_str(), path.c_str()); + + if (!asyncClient->connect(host.c_str(), port.toInt())) { + Serial.printf("Failed to open tunnel!\n"); + delete safeAsyncClient; + } + }); + + wsHandler.onConnect([](AsyncWebSocket *server, AsyncWebSocketClient *client) { + Serial.printf("Client %" PRIu32 " connected\n", client->id()); + client->binary("WebSocket connected!"); + }); + + wsHandler.onDisconnect([](AsyncWebSocket *server, uint32_t clientId) { + Serial.printf("Client %" PRIu32 " disconnected\n", clientId); + }); + + server.addHandler(&ws); + server.begin(); + Serial.println("Server started!"); +} + +static uint32_t lastHeap = 0; + +void loop() { + ws.cleanupClients(2); + +#ifdef ESP32 + uint32_t now = millis(); + if (now - lastHeap >= 2000) { + Serial.printf("Uptime: %3lu s, Free heap: %" PRIu32 "\n", millis() / 1000, ESP.getFreeHeap()); + lastHeap = now; + } +#endif + + delay(500); +} diff --git a/examples/Auth/Auth.ino b/examples/Auth/Auth.ino index c3751e0..8f5b535 100644 --- a/examples/Auth/Auth.ino +++ b/examples/Auth/Auth.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -52,7 +52,7 @@ static AsyncAuthorizationMiddleware authz([](AsyncWebServerRequest *request) { void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/CORS/CORS.ino b/examples/CORS/CORS.ino index 3be46fd..647d555 100644 --- a/examples/CORS/CORS.ino +++ b/examples/CORS/CORS.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -25,7 +25,7 @@ static AsyncCorsMiddleware cors; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/CaptivePortal/CaptivePortal.ino b/examples/CaptivePortal/CaptivePortal.ino index a872a9b..0b8c317 100644 --- a/examples/CaptivePortal/CaptivePortal.ino +++ b/examples/CaptivePortal/CaptivePortal.ino @@ -2,7 +2,7 @@ // Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -28,7 +28,7 @@ public: response->print("Captive Portal"); response->print("

This is our captive portal front page.

"); response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); #endif response->print(""); @@ -41,7 +41,7 @@ void setup() { Serial.println(); Serial.println("Configuring access point..."); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI if (!WiFi.softAP("esp-captive")) { Serial.println("Soft AP creation failed."); while (1); diff --git a/examples/CatchAllHandler/CatchAllHandler.ino b/examples/CatchAllHandler/CatchAllHandler.ino index 42a3698..fb01410 100644 --- a/examples/CatchAllHandler/CatchAllHandler.ino +++ b/examples/CatchAllHandler/CatchAllHandler.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ChunkResponse/ChunkResponse.ino b/examples/ChunkResponse/ChunkResponse.ino index e7d4838..52c31c0 100644 --- a/examples/ChunkResponse/ChunkResponse.ino +++ b/examples/ChunkResponse/ChunkResponse.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ChunkRetryResponse/ChunkRetryResponse.ino b/examples/ChunkRetryResponse/ChunkRetryResponse.ino index 48772cc..4e67edb 100644 --- a/examples/ChunkRetryResponse/ChunkRetryResponse.ino +++ b/examples/ChunkRetryResponse/ChunkRetryResponse.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -96,7 +96,7 @@ static int key = -1; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -131,9 +131,17 @@ void setup() { "/api", HTTP_POST, [](AsyncWebServerRequest *request) { // request parsing has finished + String *data = (String *)request->_tempObject; + + if (!data) { + request->send(400); + return; + } // no data ? - if (!((String *)request->_tempObject)->length()) { + if (!data->length()) { + delete data; + request->_tempObject = nullptr; request->send(400); return; } @@ -141,11 +149,16 @@ void setup() { JsonDocument doc; // deserialize and check for errors - if (deserializeJson(doc, *(String *)request->_tempObject)) { + if (deserializeJson(doc, *data)) { + delete data; + request->_tempObject = nullptr; request->send(400); return; } + delete data; + request->_tempObject = nullptr; + // start UART com: UART will send the data to the Serial console and wait for the key press triggerUART = doc["input"].as(); key = -1; diff --git a/examples/EndBegin/EndBegin.ino b/examples/EndBegin/EndBegin.ino index acfc6ff..8e91fcf 100644 --- a/examples/EndBegin/EndBegin.ino +++ b/examples/EndBegin/EndBegin.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Filters/Filters.ino b/examples/Filters/Filters.ino index 519478c..bcdb5b3 100644 --- a/examples/Filters/Filters.ino +++ b/examples/Filters/Filters.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -32,7 +32,7 @@ public: response->print("Captive Portal"); response->print("

This is out captive portal front page.

"); response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); #endif response->print(""); @@ -51,17 +51,17 @@ void setup() { "/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println("Captive portal request..."); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); #endif Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); #if ESP_IDF_VERSION_MAJOR >= 5 -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); #endif Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); #endif -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); Serial.println(WiFi.localIP() == request->client()->localIP()); Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); @@ -77,17 +77,17 @@ void setup() { "/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println("Website request..."); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); #endif Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); #if ESP_IDF_VERSION_MAJOR >= 5 -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); #endif Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); #endif -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); Serial.println(WiFi.localIP() == request->client()->localIP()); Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); @@ -113,7 +113,7 @@ void setup() { // dnsServer.stop(); // WiFi.softAPdisconnect(); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.persistent(false); WiFi.begin("IoT"); while (WiFi.status() != WL_CONNECTED) { diff --git a/examples/FlashResponse/FlashResponse.ino b/examples/FlashResponse/FlashResponse.ino index 6948cd2..5763f22 100644 --- a/examples/FlashResponse/FlashResponse.ino +++ b/examples/FlashResponse/FlashResponse.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/HeaderManipulation/HeaderManipulation.ino b/examples/HeaderManipulation/HeaderManipulation.ino index 4fe34dc..5b4c9f7 100644 --- a/examples/HeaderManipulation/HeaderManipulation.ino +++ b/examples/HeaderManipulation/HeaderManipulation.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -33,7 +33,7 @@ AsyncHeaderFreeMiddleware headerFree; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -79,6 +79,29 @@ void setup() { ) .addMiddleware(&headerFree); + // curl -v http://192.168.4.1/ + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Hello, world!"); + response->addHeader(AsyncWebHeader::parse("X-Test-1: value1")); + response->addHeader(AsyncWebHeader::parse("X-Test-2:value2")); + response->addHeader(AsyncWebHeader::parse("X-Test-3:")); + response->addHeader(AsyncWebHeader::parse("X-Test-4: ")); + response->addHeader(AsyncWebHeader::parse("")); + response->addHeader(AsyncWebHeader::parse(":")); + request->send(response); + /** +< HTTP/1.1 200 OK +< connection: close +< X-Test-1: value1 +< X-Test-2: value2 +< X-Test-3: +< X-Test-4: +< accept-ranges: none +< content-length: 13 +< content-type: text/plain + */ + }); + server.begin(); } diff --git a/examples/Headers/Headers.ino b/examples/Headers/Headers.ino index e07c515..eee87ac 100644 --- a/examples/Headers/Headers.ino +++ b/examples/Headers/Headers.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Json/Json.ino b/examples/Json/Json.ino index 0ea8892..a29fec1 100644 --- a/examples/Json/Json.ino +++ b/examples/Json/Json.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -34,7 +34,7 @@ static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/ void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -63,14 +63,27 @@ void setup() { JsonObject root = doc.to(); root["foo"] = "bar"; serializeJson(root, *response); + Serial.println(); request->send(response); }); // curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json2 // curl -v -X PUT -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json2 + // + // edge cases: + // + // curl -v -X POST -H "Content-Type: application/json" -d "1234" -H "Content-Length: 5" http://192.168.4.1/json2 => rx timeout + // curl -v -X POST -H "Content-Type: application/json" -d "1234" -H "Content-Length: 2" http://192.168.4.1/json2 => 12 + // curl -v -X POST -H "Content-Type: application/json" -d "1234" -H "Content-Length: 4" http://192.168.4.1/json2 => 1234 + // curl -v -X POST -H "Content-Type: application/json" -d "1234" -H "Content-Length: 10" http://192.168.4.1/json2 => rx timeout + // curl -v -X POST -H "Content-Type: application/json" -d "12345678" -H "Content-Length: 8" http://192.168.4.1/json2 => 12345678 + // curl -v -X POST -H "Content-Type: application/json" -d "123456789" -H "Content-Length: 8" http://192.168.4.1/json2 => 12345678 + // curl -v -X POST -H "Content-Type: application/json" -d "123456789" -H "Content-Length: 9" http://192.168.4.1/json2 => 413: Content length exceeds maximum allowed + handler->setMaxContentLength(8); handler->setMethod(HTTP_POST | HTTP_PUT); handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { serializeJson(json, Serial); + Serial.println(); AsyncJsonResponse *response = new AsyncJsonResponse(); JsonObject root = response->getRoot().to(); root["hello"] = json.as()["name"]; diff --git a/examples/Logging/Logging.ino b/examples/Logging/Logging.ino index 6485185..ae504b2 100644 --- a/examples/Logging/Logging.ino +++ b/examples/Logging/Logging.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -25,7 +25,7 @@ static AsyncLoggingMiddleware requestLogger; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/MessagePack/MessagePack.ino b/examples/MessagePack/MessagePack.ino index 4fea247..e038d03 100644 --- a/examples/MessagePack/MessagePack.ino +++ b/examples/MessagePack/MessagePack.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -34,7 +34,7 @@ static AsyncCallbackMessagePackWebHandler *handler = new AsyncCallbackMessagePac void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Middleware/Middleware.ino b/examples/Middleware/Middleware.ino index c52f949..992a0a2 100644 --- a/examples/Middleware/Middleware.ino +++ b/examples/Middleware/Middleware.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -34,7 +34,7 @@ public: void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Params/Params.ino b/examples/Params/Params.ino index 2c438a5..416218c 100644 --- a/examples/Params/Params.ino +++ b/examples/Params/Params.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -74,7 +74,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/PartitionDownloader/PartitionDownloader.ino b/examples/PartitionDownloader/PartitionDownloader.ino index 3c76366..1174640 100644 --- a/examples/PartitionDownloader/PartitionDownloader.ino +++ b/examples/PartitionDownloader/PartitionDownloader.ino @@ -7,7 +7,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -34,7 +34,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/PerfTests/PerfTests.ino b/examples/PerfTests/PerfTests.ino index 6467d2c..001512c 100644 --- a/examples/PerfTests/PerfTests.ino +++ b/examples/PerfTests/PerfTests.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -91,7 +91,7 @@ static volatile size_t requests = 0; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/RateLimit/RateLimit.ino b/examples/RateLimit/RateLimit.ino index 89d6090..5ae93e7 100644 --- a/examples/RateLimit/RateLimit.ino +++ b/examples/RateLimit/RateLimit.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -25,7 +25,7 @@ static AsyncRateLimitMiddleware rateLimit; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Redirect/Redirect.ino b/examples/Redirect/Redirect.ino index ce1b9fb..8f10557 100644 --- a/examples/Redirect/Redirect.ino +++ b/examples/Redirect/Redirect.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/RequestContinuation/RequestContinuation.ino b/examples/RequestContinuation/RequestContinuation.ino index 0584cf1..f59322e 100644 --- a/examples/RequestContinuation/RequestContinuation.ino +++ b/examples/RequestContinuation/RequestContinuation.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -34,7 +34,7 @@ static AsyncWebServerRequestPtr gpioRequest; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/RequestContinuationComplete/RequestContinuationComplete.ino b/examples/RequestContinuationComplete/RequestContinuationComplete.ino index ccd16fd..cb4a53f 100644 --- a/examples/RequestContinuationComplete/RequestContinuationComplete.ino +++ b/examples/RequestContinuationComplete/RequestContinuationComplete.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -94,7 +94,7 @@ static bool processLongRunningOperation(LongRunningOperation *op) { void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ResumableDownload/ResumableDownload.ino b/examples/ResumableDownload/ResumableDownload.ino index 373ca24..68646e7 100644 --- a/examples/ResumableDownload/ResumableDownload.ino +++ b/examples/ResumableDownload/ResumableDownload.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Rewrite/Rewrite.ino b/examples/Rewrite/Rewrite.ino index 6981b11..8dfeedc 100644 --- a/examples/Rewrite/Rewrite.ino +++ b/examples/Rewrite/Rewrite.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ServerSentEvents/ServerSentEvents.ino b/examples/ServerSentEvents/ServerSentEvents.ino index 91e2c1d..5567ec6 100644 --- a/examples/ServerSentEvents/ServerSentEvents.ino +++ b/examples/ServerSentEvents/ServerSentEvents.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -58,7 +58,7 @@ static AsyncEventSource events("/events"); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino new file mode 100644 index 0000000..cced715 --- /dev/null +++ b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// SSE example +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static const char *htmlContent PROGMEM = R"( + + + + Server-Sent Events + + + +

Open your browser console!

+ + +)"; + +static const size_t htmlContentLength = strlen_P(htmlContent); + +static AsyncWebServer server(80); +static AsyncEventSource events("/events"); + +static volatile size_t connectionCount = 0; +static volatile uint32_t timestampConnected = 0; +static constexpr uint32_t timeoutClose = 15000; + +void setup() { + Serial.begin(115200); + +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // curl -v http://192.168.4.1/ + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + // need to cast to uint8_t* + // if you do not, the const char* will be copied in a temporary String buffer + request->send(200, "text/html", (uint8_t *)htmlContent, htmlContentLength); + }); + + events.onConnect([](AsyncEventSourceClient *client) { + /** + * @brief: Purpose for a test case: count() function + * Task watchdog shall be triggered due to a self-deadlock by mutex handling of the AsyncEventSource. + * + * E (61642) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time: + * E (61642) task_wdt: - async_tcp (CPU 0/1) + * + * Resolve: using recursive_mutex insteads of mutex. + */ + connectionCount = events.count(); + + timestampConnected = millis(); + Serial.printf("SSE Client connected! ID: %" PRIu32 "\n", client->lastId()); + client->send("hello!", NULL, millis(), 1000); + Serial.printf("Number of connected clients: %u\n", connectionCount); + }); + + events.onDisconnect([](AsyncEventSourceClient *client) { + connectionCount = events.count(); + Serial.printf("SSE Client disconnected! ID: %" PRIu32 "\n", client->lastId()); + Serial.printf("Number of connected clients: %u\n", connectionCount); + }); + + server.addHandler(&events); + + server.begin(); +} + +static constexpr uint32_t deltaSSE = 3000; +static uint32_t lastSSE = 0; +static uint32_t lastHeap = 0; + +void loop() { + uint32_t now = millis(); + if (connectionCount > 0) { + if (now - lastSSE >= deltaSSE) { + events.send(String("ping-") + now, "heartbeat", now); + lastSSE = millis(); + } + + /** + * @brief: Purpose for a test case: close() function + * Task watchdog shall be triggered due to a self-deadlock by mutex handling of the AsyncEventSource. + * + * E (61642) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time: + * E (61642) task_wdt: - async_tcp (CPU 0/1) + * + * Resolve: using recursive_mutex insteads of mutex. + */ + if (now - timestampConnected >= timeoutClose) { + Serial.printf("SSE Clients close\n"); + events.close(); + } + } + +#ifdef ESP32 + if (now - lastHeap >= 2000) { + Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); + lastHeap = now; + } +#endif +} diff --git a/examples/ServerState/ServerState.ino b/examples/ServerState/ServerState.ino index 8501758..4ceddbc 100644 --- a/examples/ServerState/ServerState.ino +++ b/examples/ServerState/ServerState.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -25,7 +25,7 @@ static AsyncWebServer server2(80); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/SkipServerMiddleware/SkipServerMiddleware.ino b/examples/SkipServerMiddleware/SkipServerMiddleware.ino index d232c71..0e7f172 100644 --- a/examples/SkipServerMiddleware/SkipServerMiddleware.ino +++ b/examples/SkipServerMiddleware/SkipServerMiddleware.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -27,7 +27,7 @@ static AsyncLoggingMiddleware logging; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/SlowChunkResponse/SlowChunkResponse.ino b/examples/SlowChunkResponse/SlowChunkResponse.ino index bbf70b6..7844ad6 100644 --- a/examples/SlowChunkResponse/SlowChunkResponse.ino +++ b/examples/SlowChunkResponse/SlowChunkResponse.ino @@ -7,7 +7,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -89,7 +89,7 @@ static size_t charactersIndex = 0; void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/StaticFile/StaticFile.ino b/examples/StaticFile/StaticFile.ino index 331f287..edc2cb2 100644 --- a/examples/StaticFile/StaticFile.ino +++ b/examples/StaticFile/StaticFile.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -84,10 +84,34 @@ static const char *htmlContent PROGMEM = R"( static const size_t htmlContentLength = strlen_P(htmlContent); +// sample_html_gz.h +static const uint8_t index2_html_gz[] = { + 0x1f, 0x8b, 0x08, 0x08, 0x13, 0x45, 0x92, 0x68, 0x00, 0x03, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x32, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0xed, 0x98, 0xcb, + 0x6e, 0xdc, 0x30, 0x0c, 0x45, 0xf7, 0xf3, 0x15, 0xcc, 0xde, 0xb0, 0x91, 0xbd, 0xe1, 0x4d, 0x1f, 0x48, 0x81, 0xbc, 0x8a, 0x26, 0x2d, 0xba, 0xe4, 0x48, + 0x8c, 0x87, 0x81, 0x1e, 0x0e, 0x25, 0x19, 0xc8, 0xdf, 0x97, 0xb2, 0x67, 0x82, 0x6c, 0xfa, 0x07, 0x32, 0x0c, 0x58, 0xa6, 0xa8, 0xcb, 0x4b, 0xe9, 0xac, + 0x34, 0x5e, 0x7d, 0x7d, 0xf8, 0xf2, 0xf4, 0xf7, 0xf1, 0x1b, 0x9c, 0xb2, 0x77, 0xd3, 0x61, 0xbc, 0x7c, 0x08, 0xed, 0x74, 0x00, 0x7d, 0xc6, 0xcc, 0xd9, + 0xd1, 0xf4, 0x0b, 0xfd, 0xe2, 0x08, 0x6e, 0x9e, 0xee, 0x6e, 0xc7, 0x61, 0x0f, 0x1d, 0xc6, 0x61, 0x4f, 0x1b, 0x8f, 0xd1, 0xbe, 0x9f, 0xb3, 0x4f, 0xd7, + 0xd3, 0x0d, 0x39, 0x17, 0x3b, 0xf8, 0x13, 0xc5, 0xd9, 0x2b, 0xcd, 0xb9, 0x3e, 0x4f, 0x2d, 0xd3, 0x6d, 0x14, 0xf2, 0xc0, 0x4b, 0x2a, 0x1e, 0x6c, 0x74, + 0x51, 0x20, 0x71, 0x06, 0xf4, 0x94, 0x3b, 0x30, 0x31, 0x24, 0x32, 0x99, 0x72, 0x11, 0x40, 0xcb, 0x0b, 0x27, 0xc3, 0x61, 0x06, 0x72, 0x9c, 0x7b, 0x78, + 0x94, 0xc8, 0x01, 0xa8, 0x70, 0xf2, 0xd1, 0x76, 0xb0, 0x14, 0x29, 0x09, 0xf0, 0x12, 0xd8, 0xe4, 0xe5, 0x14, 0x83, 0x29, 0xa9, 0x83, 0x22, 0x01, 0xcf, + 0x35, 0x4c, 0x91, 0xa4, 0x89, 0x1e, 0x53, 0xc2, 0x4e, 0xb3, 0xc1, 0xb2, 0xc9, 0x1a, 0xcf, 0xea, 0x50, 0xe3, 0xaf, 0x25, 0xe5, 0x08, 0x68, 0xf6, 0x41, + 0x0f, 0x3f, 0x55, 0xee, 0xad, 0x10, 0x14, 0xe7, 0xd0, 0x9b, 0x28, 0x0b, 0xc9, 0x26, 0x8d, 0x62, 0x0a, 0x04, 0x32, 0x90, 0xa3, 0xe8, 0xfb, 0x79, 0xbe, + 0x83, 0x95, 0x1c, 0xbc, 0x90, 0x78, 0x0a, 0x55, 0x79, 0x97, 0xfc, 0xf8, 0xef, 0xe1, 0x37, 0xaf, 0xe8, 0xb5, 0x56, 0x22, 0x5b, 0x53, 0xb5, 0xdd, 0x92, + 0xb7, 0xa6, 0x76, 0x65, 0x63, 0x8a, 0x4f, 0x18, 0x6a, 0xf7, 0x73, 0xad, 0xbc, 0x4f, 0x07, 0xd6, 0x95, 0xcf, 0xb9, 0x3a, 0xde, 0x05, 0x75, 0xe0, 0x50, + 0xbb, 0x83, 0x15, 0x85, 0xf5, 0x33, 0x0b, 0xae, 0x6c, 0xb1, 0x26, 0xe3, 0xb9, 0x9b, 0x1e, 0xee, 0xab, 0x2f, 0x78, 0x41, 0xc3, 0x8e, 0x13, 0xf7, 0x5b, + 0x81, 0x1f, 0x21, 0xd3, 0x4c, 0xba, 0xa3, 0xc5, 0x54, 0xe7, 0x9f, 0x37, 0xb9, 0xb8, 0x2c, 0x6c, 0x98, 0x74, 0xe5, 0xf7, 0x92, 0x0c, 0xa9, 0xeb, 0x32, + 0x33, 0xea, 0x51, 0x78, 0xfe, 0x38, 0x17, 0x38, 0xf2, 0x91, 0x82, 0xd5, 0xce, 0x56, 0x5e, 0x49, 0x44, 0xb7, 0x31, 0x8a, 0x61, 0x70, 0x14, 0x37, 0x7d, + 0x8b, 0x0b, 0x1f, 0xd5, 0x50, 0xed, 0xa8, 0x03, 0xb6, 0x17, 0x83, 0x49, 0xcf, 0xd9, 0x16, 0xae, 0x91, 0xcd, 0x78, 0x3f, 0x0e, 0x4b, 0xc3, 0xa0, 0x61, + 0xd0, 0x30, 0x68, 0x18, 0x34, 0x0c, 0x1a, 0x06, 0x0d, 0x83, 0x86, 0x41, 0xc3, 0xa0, 0x61, 0xd0, 0x30, 0x68, 0x18, 0x34, 0x0c, 0x1a, 0x06, 0xff, 0xc1, + 0x60, 0x1c, 0xf6, 0xab, 0x85, 0x71, 0xd8, 0xee, 0x25, 0xfe, 0x01, 0xa0, 0xec, 0x78, 0xfe, 0xae, 0x10, 0x00, 0x00 +}; + +static const size_t index2_html_gz_len = sizeof(index2_html_gz); + void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -105,6 +129,13 @@ void setup() { f.close(); } + { + File f = LittleFS.open("/index2.html.gz", "w"); + assert(f); + f.write(index2_html_gz, index2_html_gz_len); + f.close(); + } + LittleFS.mkdir("/files"); { @@ -129,6 +160,9 @@ void setup() { // curl -v http://192.168.4.1/index.html server.serveStatic("/index.html", LittleFS, "/index.html"); + // curl -v http://192.168.4.1/index2.html | gunzip -c + server.serveStatic("/index2.html", LittleFS, "/index2.html"); + // Example to serve a directory content // curl -v http://192.168.4.1/base/ => serves a.txt // curl -v http://192.168.4.1/base/a.txt => serves a.txt diff --git a/examples/Templates/Templates.ino b/examples/Templates/Templates.ino index edc02c2..fdf4eb6 100644 --- a/examples/Templates/Templates.ino +++ b/examples/Templates/Templates.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -36,7 +36,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Upload/Upload.ino b/examples/Upload/Upload.ino index ceac47d..fd80bd7 100644 --- a/examples/Upload/Upload.ino +++ b/examples/Upload/Upload.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -31,7 +31,7 @@ void setup() { LittleFS.begin(); } -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/WebSocket/WebSocket.ino b/examples/WebSocket/WebSocket.ino index 9fb6ffd..c8d3727 100644 --- a/examples/WebSocket/WebSocket.ino +++ b/examples/WebSocket/WebSocket.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -25,7 +25,7 @@ static AsyncWebSocket ws("/ws"); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -102,8 +102,10 @@ void loop() { } if (now - lastHeap >= 2000) { - // cleanup disconnected clients or too many clients - ws.cleanupClients(); + 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 #ifdef ESP32 Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); diff --git a/examples/WebSocketEasy/WebSocketEasy.ino b/examples/WebSocketEasy/WebSocketEasy.ino index 12b03ce..5229910 100644 --- a/examples/WebSocketEasy/WebSocketEasy.ino +++ b/examples/WebSocketEasy/WebSocketEasy.ino @@ -6,7 +6,7 @@ // #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include #include #elif defined(ESP8266) @@ -71,7 +71,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/idf_component.yml b/idf_component.yml index 7969fda..d2afdf8 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -25,11 +25,14 @@ files: - "platformio.ini" - "pre-commit.requirements.txt" dependencies: + espressif/arduino-esp32: + version: "^3.1.1" + require: public esp32async/asynctcp: - version: "^3.3.8" + version: "^3.4.7" require: public bblanchon/arduinojson: - version: "^7.3.1" + version: "^7.4.2" require: public examples: - path: ./idf_component_examples/catchall diff --git a/idf_component_examples/catchall/main/main.cpp b/idf_component_examples/catchall/main/main.cpp index c491588..0ba61ec 100644 --- a/idf_component_examples/catchall/main/main.cpp +++ b/idf_component_examples/catchall/main/main.cpp @@ -78,7 +78,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/idf_component_examples/serversentevents/main/main.cpp b/idf_component_examples/serversentevents/main/main.cpp index 59a1f59..b877f54 100644 --- a/idf_component_examples/serversentevents/main/main.cpp +++ b/idf_component_examples/serversentevents/main/main.cpp @@ -50,7 +50,7 @@ static AsyncEventSource events("/events"); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/idf_component_examples/websocket/main/main.cpp b/idf_component_examples/websocket/main/main.cpp index 843d1a4..9d4e46a 100644 --- a/idf_component_examples/websocket/main/main.cpp +++ b/idf_component_examples/websocket/main/main.cpp @@ -17,7 +17,7 @@ static AsyncWebSocket ws("/ws"); void setup() { Serial.begin(115200); -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/library.json b/library.json_ similarity index 88% rename from library.json rename to library.json_ index bd9b884..4d13d28 100644 --- a/library.json +++ b/library.json_ @@ -1,6 +1,6 @@ { "name": "ESPAsyncWebServer", - "version": "3.7.5", + "version": "3.8.0", "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", "keywords": "http,async,websocket,webserver", "homepage": "https://github.com/ESP32Async/ESPAsyncWebServer", @@ -18,14 +18,18 @@ "platforms": [ "espressif32", "espressif8266", - "raspberrypi" + "raspberrypi", + "libretiny" ], "dependencies": [ { "owner": "ESP32Async", "name": "AsyncTCP", - "version": "^3.3.8", - "platforms": "espressif32" + "version": "^3.4.7", + "platforms": [ + "espressif32", + "libretiny" + ] }, { "owner": "ESP32Async", diff --git a/library.properties b/library.properties index 4a1b210..774972b 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=ESP Async WebServer includes=ESPAsyncWebServer.h -version=3.7.5 +version=3.8.0 author=ESP32Async maintainer=ESP32Async sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 diff --git a/pioarduino_examples/IncreaseMaxSockets/platformio.ini b/pioarduino_examples/IncreaseMaxSockets/platformio.ini index 08d7a50..e42d0a7 100644 --- a/pioarduino_examples/IncreaseMaxSockets/platformio.ini +++ b/pioarduino_examples/IncreaseMaxSockets/platformio.ini @@ -1,6 +1,6 @@ [env] framework = arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip build_flags = -Og -Wall -Wextra @@ -17,7 +17,7 @@ monitor_filters = esp32_exception_decoder, log2file lib_compat_mode = strict lib_ldf_mode = chain lib_deps = - ESP32Async/AsyncTCP @ 3.3.8 + ESP32Async/AsyncTCP @ 3.4.7 ESP32Async/ESpAsyncWebServer @ 3.7.0 custom_sdkconfig = CONFIG_LWIP_MAX_ACTIVE_TCP=32 diff --git a/platformio.ini b/platformio.ini index a0129d4..4695893 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,6 +2,7 @@ default_envs = arduino-2, arduino-3, esp8266, raspberrypi lib_dir = . ; src_dir = examples/AsyncResponseStream +; src_dir = examples/AsyncTunnel ; src_dir = examples/Auth ; src_dir = examples/CaptivePortal ; src_dir = examples/CatchAllHandler @@ -37,7 +38,7 @@ src_dir = examples/PerfTests [env] framework = arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip board = esp32dev build_flags = -Og @@ -58,24 +59,26 @@ monitor_filters = esp32_exception_decoder, log2file lib_compat_mode = strict lib_ldf_mode = chain lib_deps = - bblanchon/ArduinoJson @ 7.3.1 - ESP32Async/AsyncTCP @ 3.3.8 + bblanchon/ArduinoJson @ 7.4.2 + ESP32Async/AsyncTCP @ 3.4.7 board_build.partitions = partitions-4MB.csv board_build.filesystem = littlefs [env:arduino-2] -platform = espressif32@6.10.0 +platform = espressif32@6.12.0 [env:arduino-3] +; board = esp32-p4 +; board = esp32-h2-devkitm-1 -[env:arduino-3-latest] +[env:arduino-rc] platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip [env:arduino-3-no-json] lib_deps = - ESP32Async/AsyncTCP @ 3.3.8 + ESP32Async/AsyncTCP @ 3.4.7 -[env:arduino-3-latest-asynctcp] +[env:arduino-rc-asynctcp] lib_deps = https://github.com/ESP32Async/AsyncTCP @@ -93,7 +96,7 @@ platform = espressif8266 ; board = huzzah board = d1_mini lib_deps = - bblanchon/ArduinoJson @ 7.3.1 + bblanchon/ArduinoJson @ 7.4.2 ESP32Async/ESPAsyncTCP @ 2.0.0 [env:raspberrypi] @@ -108,25 +111,36 @@ lib_ignore = build_flags = ${env.build_flags} -Wno-missing-field-initializers +[env:libretiny] +platform = libretiny @ ^1.9.1 +board = generic-bk7231n-qfn32-tuya +; board = generic-rtl8710bn-2mb-788k +lib_compat_mode = off +lib_deps = + ESP32Async/AsyncTCP @ 3.4.3 +; use FreeRTOS v9.0.0 for RTL8710BN +; (BK7231 already uses it) +custom_versions.freertos = 9.0.0 + ; CI [env:ci-arduino-2] -platform = espressif32@6.10.0 +platform = espressif32@6.12.0 board = ${sysenv.PIO_BOARD} [env:ci-arduino-3] board = ${sysenv.PIO_BOARD} -[env:ci-arduino-3-latest] +[env:ci-arduino-rc] platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip board = ${sysenv.PIO_BOARD} [env:ci-arduino-3-no-json] board = ${sysenv.PIO_BOARD} lib_deps = - ESP32Async/AsyncTCP @ 3.3.8 + ESP32Async/AsyncTCP @ 3.4.7 -[env:ci-arduino-3-latest-asynctcp] +[env:ci-arduino-rc-asynctcp] lib_deps = https://github.com/ESP32Async/AsyncTCP @@ -139,7 +153,7 @@ build_flags = ${env.build_flags} platform = espressif8266 board = ${sysenv.PIO_BOARD} lib_deps = - bblanchon/ArduinoJson @ 7.3.1 + bblanchon/ArduinoJson @ 7.4.2 ESP32Async/ESPAsyncTCP @ 2.0.0 [env:ci-raspberrypi] @@ -153,3 +167,13 @@ lib_ignore = lwIP_ESPHost build_flags = ${env.build_flags} -Wno-missing-field-initializers + +[env:ci-libretiny] +platform = libretiny @ ^1.9.1 +board = ${sysenv.PIO_BOARD} +lib_compat_mode = off +lib_deps = +; add DNS server library for LibreTiny + DNSServer + ESP32Async/AsyncTCP @ 3.4.3 +custom_versions.freertos = 9.0.0 diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 797c5d1..2ebfa2d 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -193,7 +193,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A AsyncEventSourceClient::~AsyncEventSourceClient() { #ifdef ESP32 - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _messageQueue.clear(); close(); @@ -211,7 +211,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) { #ifdef ESP32 // length() is not thread-safe, thus acquiring the lock before this call.. - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _messageQueue.emplace_back(message, len); @@ -241,7 +241,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { #ifdef ESP32 // length() is not thread-safe, thus acquiring the lock before this call.. - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _messageQueue.emplace_back(std::move(msg)); @@ -261,7 +261,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { #ifdef ESP32 // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif // adjust in-flight len @@ -290,7 +290,7 @@ void AsyncEventSourceClient::_onPoll() { if (_messageQueue.size()) { #ifdef ESP32 // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _runQueue(); } @@ -367,7 +367,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) { return; } #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif _clients.emplace_back(client); if (_connectcb) { @@ -382,7 +382,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) { _disconnectcb(client); } #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif for (auto i = _clients.begin(); i != _clients.end(); ++i) { if (i->get() == client) { @@ -398,10 +398,15 @@ void AsyncEventSource::close() { // iterator should remain valid even when AsyncEventSource::_handleDisconnect() // is called very early #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif for (const auto &c : _clients) { if (c->connected()) { + /** + * @brief: Fix self-deadlock by using recursive_mutex instead. + * Due to c->close() shall call the callback function _onDisconnect() + * The calling flow _onDisconnect() --> _handleDisconnect() --> deadlock + */ c->close(); } } @@ -412,7 +417,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const { size_t aql = 0; uint32_t nConnectedClients = 0; #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif if (!_clients.size()) { return 0; @@ -430,7 +435,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const { AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { AsyncEvent_SharedData_t shared_msg = std::make_shared(generateEventMessage(message, event, id, reconnect)); #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif size_t hits = 0; size_t miss = 0; @@ -446,7 +451,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c size_t AsyncEventSource::count() const { #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif size_t n_clients{0}; for (const auto &i : _clients) { diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index fe4c840..ccfda95 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -6,8 +6,13 @@ #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include +#ifdef LIBRETINY +#ifdef round +#undef round +#endif +#endif #include #ifndef SSE_MAX_QUEUED_MESSAGES #define SSE_MAX_QUEUED_MESSAGES 32 @@ -129,7 +134,7 @@ private: size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer std::list _messageQueue; #ifdef ESP32 - mutable std::mutex _lockmq; + mutable std::recursive_mutex _lockmq; #endif bool _queueMessage(const char *message, size_t len); bool _queueMessage(AsyncEvent_SharedData_t &&msg); @@ -230,7 +235,7 @@ private: #ifdef ESP32 // Same as for individual messages, protect mutations of _clients list // since simultaneous access from different tasks is possible - mutable std::mutex _client_queue_lock; + mutable std::recursive_mutex _client_queue_lock; #endif ArEventHandlerFunction _connectcb = nullptr; ArEventHandlerFunction _disconnectcb = nullptr; diff --git a/src/AsyncJson.cpp b/src/AsyncJson.cpp index b8d014b..8381f75 100644 --- a/src/AsyncJson.cpp +++ b/src/AsyncJson.cpp @@ -113,53 +113,77 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) { if (_onRequest) { + // GET request: if (request->method() == HTTP_GET) { JsonVariant json; _onRequest(request, json); return; - } else if (request->_tempObject != NULL) { + } + + // POST / PUT / ... requests: + // check if JSON body is too large, if it is, don't deserialize + if (request->contentLength() > _maxContentLength) { +#ifdef ESP32 + log_e("Content length exceeds maximum allowed"); +#endif + request->send(413); + return; + } + + if (request->_tempObject == NULL) { + // there is no body + request->send(400); + return; + } #if ARDUINOJSON_VERSION_MAJOR == 5 - DynamicJsonBuffer jsonBuffer; - JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject)); - if (json.success()) { + DynamicJsonBuffer jsonBuffer; + JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject); + if (json.success()) { #elif ARDUINOJSON_VERSION_MAJOR == 6 - DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); + DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); + DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject); + if (!error) { + JsonVariant json = jsonBuffer.as(); #else - JsonDocument jsonBuffer; - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); + JsonDocument jsonBuffer; + DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject); + if (!error) { + JsonVariant json = jsonBuffer.as(); #endif - _onRequest(request, json); - return; - } + _onRequest(request, json); + } else { + // error parsing the body + request->send(400); } - request->send(_contentLength > _maxContentLength ? 413 : 400); - } else { - request->send(500); } } void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); + // ignore callback if size is larger than maxContentLength + if (total > _maxContentLength) { + return; + } + + if (index == 0) { + // this check allows request->_tempObject to be initialized from a middleware if (request->_tempObject == NULL) { + request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string + if (request->_tempObject == NULL) { #ifdef ESP32 - log_e("Failed to allocate"); + log_e("Failed to allocate"); #endif - request->abort(); - return; + request->abort(); + return; + } } } + if (request->_tempObject != NULL) { - memcpy((uint8_t *)(request->_tempObject) + index, data, len); + uint8_t *buffer = (uint8_t *)request->_tempObject; + memcpy(buffer + index, data, len); } } } diff --git a/src/AsyncJson.h b/src/AsyncJson.h index b12894b..2194069 100644 --- a/src/AsyncJson.h +++ b/src/AsyncJson.h @@ -4,8 +4,14 @@ #ifndef ASYNC_JSON_H_ #define ASYNC_JSON_H_ +#if __has_include("ArduinoJson.h") #include +#if ARDUINOJSON_VERSION_MAJOR >= 5 #define ASYNC_JSON_SUPPORT 1 +#else +#define ASYNC_JSON_SUPPORT 0 +#endif // ARDUINOJSON_VERSION_MAJOR >= 5 +#endif // __has_include("ArduinoJson.h") #if ASYNC_JSON_SUPPORT == 1 #include @@ -73,7 +79,6 @@ protected: String _uri; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; - size_t _contentLength; #if ARDUINOJSON_VERSION_MAJOR == 6 size_t maxJsonBufferSize; #endif diff --git a/src/AsyncWebHeader.cpp b/src/AsyncWebHeader.cpp index 6d82f74..fdeb74a 100644 --- a/src/AsyncWebHeader.cpp +++ b/src/AsyncWebHeader.cpp @@ -3,30 +3,32 @@ #include -AsyncWebHeader::AsyncWebHeader(const String &data) { +const AsyncWebHeader AsyncWebHeader::parse(const char *data) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers + // In HTTP/1.X, a header is a case-insensitive name followed by a colon, then optional whitespace which will be ignored, and finally by its value if (!data) { - return; + return AsyncWebHeader(); // nullptr } - int index = data.indexOf(':'); - if (index < 0) { - return; + if (data[0] == '\0') { + return AsyncWebHeader(); // empty string } - _name = data.substring(0, index); - _value = data.substring(index + 2); -} - -String AsyncWebHeader::toString() const { - String str; - if (str.reserve(_name.length() + _value.length() + 2)) { - str.concat(_name); - str.concat((char)0x3a); - str.concat((char)0x20); - str.concat(_value); - str.concat(asyncsrv::T_rn); - } else { -#ifdef ESP32 - log_e("Failed to allocate"); -#endif - } - return str; + if (strchr(data, '\n') || strchr(data, '\r')) { + return AsyncWebHeader(); // Invalid header format + } + const char *colon = strchr(data, ':'); + if (!colon) { + return AsyncWebHeader(); // separator not found + } + if (colon == data) { + return AsyncWebHeader(); // Header name cannot be empty + } + const char *startOfValue = colon + 1; // Skip the colon + // skip one optional whitespace after the colon + if (*startOfValue == ' ') { + startOfValue++; + } + String name; + name.reserve(colon - data); + name.concat(data, colon - data); + return AsyncWebHeader(name, String(startOfValue)); } diff --git a/src/AsyncWebServerRequest.cpp b/src/AsyncWebServerRequest.cpp new file mode 100644 index 0000000..18b4da0 --- /dev/null +++ b/src/AsyncWebServerRequest.cpp @@ -0,0 +1,85 @@ +#include + +/** + * @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching. + * + * This method serves files over HTTP from the provided filesystem. If a compressed version of the file + * (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file. + * It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified` + * if the client's `If-None-Match` header matches the generated ETag. + * + * @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.). + * @param path Path to the file to be served. + * @param contentType Optional MIME type of the file to be sent. + * If contentType is "" it will be obtained from the file extension + * @param download If true, forces the file to be sent as a download. + * @param callback Optional template processor for dynamic content generation. + * Templates will not be processed in compressed files. + * + * @note If neither the file nor its compressed version exists, responds with `404 Not Found`. + */ +void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) { + // Check uncompressed file first + if (fs.exists(path)) { + send(beginResponse(fs, path, contentType, download, callback)); + return; + } + + // Handle compressed version + const String gzPath = path + asyncsrv::T__gz; + File gzFile = fs.open(gzPath, fs::FileOpenMode::read); + + // Compressed file not found or invalid + if (!gzFile.seek(gzFile.size() - 8)) { + send(404); + gzFile.close(); + return; + } + + // ETag validation + if (this->hasHeader(asyncsrv::T_INM)) { + // Generate server ETag from CRC in gzip trailer + uint8_t crcInTrailer[4]; + gzFile.read(crcInTrailer, 4); + char serverETag[9]; + _getEtag(crcInTrailer, serverETag); + + // Compare with client's ETag + const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM); + if (inmHeader && inmHeader->value() == serverETag) { + gzFile.close(); + this->send(304); // Not Modified + return; + } + } + + // Send compressed file response + gzFile.close(); + send(beginResponse(fs, path, contentType, download, callback)); +} + +/** + * @brief Generates an ETag string from a 4-byte trailer + * + * This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes. + * + * @param trailer[4] Input array of 4 bytes to convert to hexadecimal + * @param serverETag Output buffer to store the ETag + * Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator) + */ +void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) { + static constexpr char hexChars[] = "0123456789ABCDEF"; + + uint32_t data; + memcpy(&data, trailer, 4); + + serverETag[0] = hexChars[(data >> 4) & 0x0F]; + serverETag[1] = hexChars[data & 0x0F]; + serverETag[2] = hexChars[(data >> 12) & 0x0F]; + serverETag[3] = hexChars[(data >> 8) & 0x0F]; + serverETag[4] = hexChars[(data >> 20) & 0x0F]; + serverETag[5] = hexChars[(data >> 16) & 0x0F]; + serverETag[6] = hexChars[(data >> 28)]; + serverETag[7] = hexChars[(data >> 24) & 0x0F]; + serverETag[8] = '\0'; +} diff --git a/src/AsyncWebServerVersion.h b/src/AsyncWebServerVersion.h index 4f3fdc4..3a7a34c 100644 --- a/src/AsyncWebServerVersion.h +++ b/src/AsyncWebServerVersion.h @@ -10,9 +10,9 @@ extern "C" { /** Major version number (X.x.x) */ #define ASYNCWEBSERVER_VERSION_MAJOR 3 /** Minor version number (x.X.x) */ -#define ASYNCWEBSERVER_VERSION_MINOR 7 +#define ASYNCWEBSERVER_VERSION_MINOR 8 /** Patch version number (x.x.X) */ -#define ASYNCWEBSERVER_VERSION_PATCH 5 +#define ASYNCWEBSERVER_VERSION_PATCH 0 /** * Macro to convert version number into an integer diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 3f91ae7..0cd1749 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -17,6 +17,8 @@ #include #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266) #include +#elif defined(LIBRETINY) +#include #endif using namespace asyncsrv; @@ -118,6 +120,11 @@ size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool return len; } +size_t AsyncWebSocketControl::send(AsyncClient *client) { + _finished = true; + return webSocketSendFrame(client, true, _opcode & 0x0F, _mask, _data, _len); +} + /* * AsyncWebSocketMessageBuffer */ @@ -144,65 +151,6 @@ bool AsyncWebSocketMessageBuffer::reserve(size_t size) { return _buffer->capacity() >= size; } -/* - * Control Frame - */ - -class AsyncWebSocketControl { -private: - uint8_t _opcode; - uint8_t *_data; - size_t _len; - bool _mask; - bool _finished; - -public: - AsyncWebSocketControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false) - : _opcode(opcode), _len(len), _mask(len && mask), _finished(false) { - if (data == NULL) { - _len = 0; - } - if (_len) { - if (_len > 125) { - _len = 125; - } - - _data = (uint8_t *)malloc(_len); - - if (_data == NULL) { -#ifdef ESP32 - log_e("Failed to allocate"); -#endif - _len = 0; - } else { - memcpy(_data, data, len); - } - } else { - _data = NULL; - } - } - - ~AsyncWebSocketControl() { - if (_data != NULL) { - free(_data); - } - } - - bool finished() const { - return _finished; - } - uint8_t opcode() { - return _opcode; - } - uint8_t len() { - return _len + 2; - } - size_t send(AsyncClient *client) { - _finished = true; - return webSocketSendFrame(client, true, _opcode & 0x0F, _mask, _data, _len); - } -}; - /* * AsyncWebSocketMessage Message */ @@ -333,7 +281,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async AsyncWebSocketClient::~AsyncWebSocketClient() { { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif _messageQueue.clear(); _controlQueue.clear(); @@ -351,7 +299,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _lastMessageTime = millis(); #ifdef ESP32 - std::lock_guard lock(_lock); + std::unique_lock lock(_lock); #endif if (!_controlQueue.empty()) { @@ -362,6 +310,14 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _controlQueue.pop_front(); _status = WS_DISCONNECTED; if (_client) { +#ifdef ESP32 + /* + Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. + Due to _client->close(true) shall call the callback function _onDisconnect() + The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() + */ + lock.unlock(); +#endif _client->close(true); } return; @@ -385,7 +341,7 @@ void AsyncWebSocketClient::_onPoll() { } #ifdef ESP32 - std::unique_lock lock(_lock); + std::unique_lock lock(_lock); #endif if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { _runQueue(); @@ -415,21 +371,21 @@ void AsyncWebSocketClient::_runQueue() { bool AsyncWebSocketClient::queueIsFull() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); } size_t AsyncWebSocketClient::queueLen() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return _messageQueue.size(); } bool AsyncWebSocketClient::canSend() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; } @@ -440,7 +396,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si } #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif _controlQueue.emplace_back(opcode, data, len, mask); @@ -458,7 +414,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint } #ifdef ESP32 - std::lock_guard lock(_lock); + std::unique_lock lock(_lock); #endif if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { @@ -466,6 +422,14 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint _status = WS_DISCONNECTED; if (_client) { +#ifdef ESP32 + /* + Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. + Due to _client->close(true) shall call the callback function _onDisconnect() + The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() + */ + lock.unlock(); +#endif _client->close(true); } @@ -551,6 +515,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) { void AsyncWebSocketClient::_onDisconnect() { // Serial.println("onDis"); _client = nullptr; + _server->_handleDisconnect(this); } void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { @@ -857,6 +822,16 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) return &_clients.back(); } +void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) { + const auto client_id = client->id(); + const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [client_id](const AsyncWebSocketClient &c) { + return c.id() == client_id; + }); + if (iter != std::end(_clients)) { + _clients.erase(iter); + } +} + bool AsyncWebSocket::availableForWriteAll() { return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { return c.queueIsFull(); @@ -1300,11 +1275,20 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket } k.concat(key); k.concat(WS_STR_UUID); +#ifdef LIBRETINY + mbedtls_sha1_context ctx; + mbedtls_sha1_init(&ctx); + mbedtls_sha1_starts(&ctx); + mbedtls_sha1_update(&ctx, (const uint8_t *)k.c_str(), k.length()); + mbedtls_sha1_finish(&ctx, hash); + mbedtls_sha1_free(&ctx); +#else SHA1Builder sha1; sha1.begin(); sha1.add((const uint8_t *)k.c_str(), k.length()); sha1.calculate(); sha1.getBytes(hash); +#endif #endif base64_encodestate _state; base64_init_encodestate(&_state); diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 2318834..46cdb5e 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -5,8 +5,14 @@ #define ASYNCWEBSOCKET_H_ #include -#ifdef ESP32 + +#if defined(ESP32) || defined(LIBRETINY) #include +#ifdef LIBRETINY +#ifdef round +#undef round +#endif +#endif #include #ifndef WS_MAX_QUEUED_MESSAGES #define WS_MAX_QUEUED_MESSAGES 32 @@ -47,7 +53,62 @@ using AsyncWebSocketSharedBuffer = std::shared_ptr>; class AsyncWebSocket; class AsyncWebSocketResponse; class AsyncWebSocketClient; -class AsyncWebSocketControl; + +/* + * Control Frame + */ + +class AsyncWebSocketControl { +private: + uint8_t _opcode; + uint8_t *_data; + size_t _len; + bool _mask; + bool _finished; + +public: + AsyncWebSocketControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false) + : _opcode(opcode), _len(len), _mask(len && mask), _finished(false) { + if (data == NULL) { + _len = 0; + } + if (_len) { + if (_len > 125) { + _len = 125; + } + + _data = (uint8_t *)malloc(_len); + + if (_data == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + _len = 0; + } else { + memcpy(_data, data, len); + } + } else { + _data = NULL; + } + } + + ~AsyncWebSocketControl() { + if (_data != NULL) { + free(_data); + } + } + + bool finished() const { + return _finished; + } + uint8_t opcode() { + return _opcode; + } + uint8_t len() { + return _len + 2; + } + size_t send(AsyncClient *client); +}; typedef struct { /** Message type as defined by enum AwsFrameType. @@ -152,7 +213,7 @@ private: uint32_t _clientId; AwsClientStatus _status; #ifdef ESP32 - mutable std::mutex _lock; + mutable std::recursive_mutex _lock; #endif std::deque _controlQueue; std::deque _messageQueue; @@ -385,6 +446,7 @@ public: return _cNextId++; } AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); + void _handleDisconnect(AsyncWebSocketClient *client); void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); bool canHandle(AsyncWebServerRequest *request) const override final; void handleRequest(AsyncWebServerRequest *request) override final; diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 24233cd..0cf1323 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -15,16 +15,13 @@ #include #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include -#include #elif defined(ESP8266) -#include #include #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #include #include -#include #include #else #error Platform not supported @@ -138,12 +135,20 @@ private: String _value; public: + AsyncWebHeader() {} AsyncWebHeader(const AsyncWebHeader &) = default; + AsyncWebHeader(AsyncWebHeader &&) = default; AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {} AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {} - AsyncWebHeader(const String &data); + +#ifndef ESP8266 + [[deprecated("Use AsyncWebHeader::parse(data) instead")]] +#endif + AsyncWebHeader(const String &data) + : AsyncWebHeader(parse(data)){}; AsyncWebHeader &operator=(const AsyncWebHeader &) = default; + AsyncWebHeader &operator=(AsyncWebHeader &&other) = default; const String &name() const { return _name; @@ -151,7 +156,18 @@ public: const String &value() const { return _value; } + String toString() const; + + // returns true if the header is valid + operator bool() const { + return _name.length(); + } + + static const AsyncWebHeader parse(const String &data) { + return parse(data.c_str()); + } + static const AsyncWebHeader parse(const char *data); }; /* @@ -187,6 +203,7 @@ class AsyncWebServerRequest { using FS = fs::FS; friend class AsyncWebServer; friend class AsyncCallbackWebHandler; + friend class AsyncFileResponse; private: AsyncClient *_client; @@ -258,6 +275,8 @@ private: void _send(); void _runMiddlewareChain(); + static void _getEtag(uint8_t trailer[4], char *serverETag); + public: File _tempFile; void *_tempObject; @@ -370,13 +389,7 @@ public: send(beginResponse(code, contentType, content, len, callback)); } - void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { - if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) { - send(beginResponse(fs, path, contentType, download, callback)); - } else { - send(404); - } - } + void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { send(fs, path, contentType.c_str(), download, callback); } @@ -1041,6 +1054,10 @@ public: setContentType(type.c_str()); } void setContentType(const char *type); + bool addHeader(AsyncWebHeader &&header, bool replaceExisting = true); + bool addHeader(const AsyncWebHeader &header, bool replaceExisting = true) { + return header && addHeader(header.name(), header.value(), replaceExisting); + } bool addHeader(const char *name, const char *value, bool replaceExisting = true); bool addHeader(const String &name, const String &value, bool replaceExisting = true) { return addHeader(name.c_str(), value.c_str(), replaceExisting); diff --git a/src/Middleware.cpp b/src/Middleware.cpp index 890303d..5e9c3c2 100644 --- a/src/Middleware.cpp +++ b/src/Middleware.cpp @@ -172,7 +172,11 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex return; } _out->print(F("* Connection from ")); +#ifndef LIBRETINY _out->print(request->client()->remoteIP().toString()); +#else + _out->print(request->client()->remoteIP()); +#endif _out->print(':'); _out->println(request->client()->remotePort()); _out->print('>'); diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index acfc7c0..21dba00 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -209,11 +209,14 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { char buf[len]; char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10); etag = ret ? String(ret) : String(request->_tempFile.size()); +#elif defined(LIBRETINY) + long val = lw ^ request->_tempFile.size(); + etag = String(val); #else etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp #endif } else { -#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) etag = String(request->_tempFile.size()); #else etag = request->_tempFile.size(); diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 8b735af..ff4cf4c 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -22,10 +22,10 @@ enum { }; AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c) - : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(), - _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), - _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), - _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { + : _client(c), _server(s), _handler(NULL), _response(NULL), _onDisconnectfn(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), + _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), + _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), + _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { c->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; @@ -341,10 +341,10 @@ bool AsyncWebServerRequest::_parseReqHead() { } bool AsyncWebServerRequest::_parseReqHeader() { - int index = _temp.indexOf(':'); - if (index) { - String name(_temp.substring(0, index)); - String value(_temp.substring(index + 2)); + AsyncWebHeader header = AsyncWebHeader::parse(_temp); + if (header) { + const String &name = header.name(); + const String &value = header.value(); if (name.equalsIgnoreCase(T_Host)) { _host = value; } else if (name.equalsIgnoreCase(T_Content_Type)) { @@ -392,9 +392,9 @@ bool AsyncWebServerRequest::_parseReqHeader() { _reqconntype = RCT_EVENT; } } - _headers.emplace_back(name, value); + _headers.emplace_back(std::move(header)); } -#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) // Ancient PRI core does not have String::clear() method 8-() _temp = emptyString; #else @@ -419,7 +419,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { _params.emplace_back(name, urlDecode(value), true); } -#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) // Ancient PRI core does not have String::clear() method 8-() _temp = emptyString; #else diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index 6408625..2b9318a 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -75,7 +75,6 @@ class AsyncFileResponse : public AsyncAbstractResponse { private: File _content; - String _path; void _setContentTypeFromPath(const String &path); public: diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 97981bd..2878a2c 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -6,17 +6,6 @@ using namespace asyncsrv; -// Since ESP8266 does not link memchr by default, here's its implementation. -void *memchr(void *ptr, int ch, size_t count) { - unsigned char *p = static_cast(ptr); - while (count--) { - if (*p++ == static_cast(ch)) { - return --p; - } - } - return nullptr; -} - /* * Abstract Response * @@ -134,6 +123,30 @@ bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) { return false; } +bool AsyncWebServerResponse::addHeader(AsyncWebHeader &&header, bool replaceExisting) { + if (!header) { + return false; // invalid header + } + for (auto i = _headers.begin(); i != _headers.end(); ++i) { + if (i->name().equalsIgnoreCase(header.name())) { + // header already set + if (replaceExisting) { + // remove, break and add the new one + _headers.erase(i); + break; + } else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name + // do not update + return false; + } else { + break; // accept multiple headers with the same name + } + } + } + // header was not found found, or existing one was removed + _headers.emplace_back(std::move(header)); + return true; +} + bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) { for (auto i = _headers.begin(); i != _headers.end(); ++i) { if (i->name().equalsIgnoreCase(name)) { @@ -595,6 +608,16 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size * File Response * */ +/** + * @brief Sets the content type based on the file path extension + * + * This method determines the appropriate MIME content type for a file based on its + * file extension. It supports both external content type functions (if available) + * and an internal mapping of common file extensions to their corresponding MIME types. + * + * @param path The file path string from which to extract the extension + * @note The method modifies the internal _contentType member variable + */ void AsyncFileResponse::_setContentTypeFromPath(const String &path) { #if HAVE_EXTERN_GET_Content_Type_FUNCTION #ifndef ESP8266 @@ -604,90 +627,138 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) { #endif _contentType = getContentType(path); #else - if (path.endsWith(T__html)) { + const char *cpath = path.c_str(); + const char *dot = strrchr(cpath, '.'); + + if (!dot) { + _contentType = T_application_octet_stream; + return; + } + + if (strcmp(dot, T__html) == 0 || strcmp(dot, T__htm) == 0) { _contentType = T_text_html; - } else if (path.endsWith(T__htm)) { - _contentType = T_text_html; - } else if (path.endsWith(T__css)) { + } else if (strcmp(dot, T__css) == 0) { _contentType = T_text_css; - } else if (path.endsWith(T__json)) { - _contentType = T_application_json; - } else if (path.endsWith(T__js)) { + } else if (strcmp(dot, T__js) == 0) { _contentType = T_application_javascript; - } else if (path.endsWith(T__png)) { + } else if (strcmp(dot, T__json) == 0) { + _contentType = T_application_json; + } else if (strcmp(dot, T__png) == 0) { _contentType = T_image_png; - } else if (path.endsWith(T__gif)) { - _contentType = T_image_gif; - } else if (path.endsWith(T__jpg)) { - _contentType = T_image_jpeg; - } else if (path.endsWith(T__ico)) { + } else if (strcmp(dot, T__ico) == 0) { _contentType = T_image_x_icon; - } else if (path.endsWith(T__svg)) { + } else if (strcmp(dot, T__svg) == 0) { _contentType = T_image_svg_xml; - } else if (path.endsWith(T__eot)) { - _contentType = T_font_eot; - } else if (path.endsWith(T__woff)) { - _contentType = T_font_woff; - } else if (path.endsWith(T__woff2)) { + } else if (strcmp(dot, T__jpg) == 0) { + _contentType = T_image_jpeg; + } else if (strcmp(dot, T__webp) == 0) { + _contentType = T_image_webp; + } else if (strcmp(dot, T__avif) == 0) { + _contentType = T_image_avif; + } else if (strcmp(dot, T__gif) == 0) { + _contentType = T_image_gif; + } else if (strcmp(dot, T__woff2) == 0) { _contentType = T_font_woff2; - } else if (path.endsWith(T__ttf)) { + } else if (strcmp(dot, T__woff) == 0) { + _contentType = T_font_woff; + } else if (strcmp(dot, T__ttf) == 0) { _contentType = T_font_ttf; - } else if (path.endsWith(T__xml)) { + } else if (strcmp(dot, T__xml) == 0) { _contentType = T_text_xml; - } else if (path.endsWith(T__pdf)) { + } else if (strcmp(dot, T__pdf) == 0) { _contentType = T_application_pdf; - } else if (path.endsWith(T__zip)) { - _contentType = T_application_zip; - } else if (path.endsWith(T__gz)) { - _contentType = T_application_x_gzip; - } else { + } else if (strcmp(dot, T__mp4) == 0) { + _contentType = T_video_mp4; + } else if (strcmp(dot, T__opus) == 0) { + _contentType = T_audio_opus; + } else if (strcmp(dot, T__webm) == 0) { + _contentType = T_video_webm; + } else if (strcmp(dot, T__txt) == 0) { _contentType = T_text_plain; + } else { + _contentType = T_application_octet_stream; } #endif } +/** + * @brief Constructor for AsyncFileResponse that handles file serving with compression support + * + * This constructor creates an AsyncFileResponse object that can serve files from a filesystem, + * with automatic fallback to gzip-compressed versions if the original file is not found. + * It also handles ETag generation for caching and supports both inline and download modes. + * + * @param fs Reference to the filesystem object used to open files + * @param path Path to the file to be served (without compression extension) + * @param contentType MIME type of the file content (empty string for auto-detection) + * @param download If true, file will be served as download attachment; if false, as inline content + * @param callback Template processor callback for dynamic content processing + */ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { - _code = 200; - _path = path; - if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { - _path = _path + T__gz; - addHeader(T_Content_Encoding, T_gzip, false); - _callback = nullptr; // Unable to process zipped templates - _sendContentLength = true; - _chunked = false; + // Try to open the uncompressed version first + _content = fs.open(path, fs::FileOpenMode::read); + if (_content.available()) { + _contentLength = _content.size(); + } else { + // Try to open the compressed version (.gz) + String gzPath; + uint16_t pathLen = path.length(); + gzPath.reserve(pathLen + 3); + gzPath.concat(path); + gzPath.concat(asyncsrv::T__gz); + _content = fs.open(gzPath, fs::FileOpenMode::read); + _contentLength = _content.size(); + + if (_content.seek(_contentLength - 8)) { + addHeader(T_Content_Encoding, T_gzip, false); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _chunked = false; + + // Add ETag and cache headers + uint8_t crcInTrailer[4]; + _content.read(crcInTrailer, sizeof(crcInTrailer)); + char serverETag[9]; + AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag); + addHeader(T_ETag, serverETag, true); + addHeader(T_Cache_Control, T_no_cache, true); + + _content.seek(0); + } else { + // File is corrupted or invalid + _code = 404; + return; + } } - _content = fs.open(_path, fs::FileOpenMode::read); - _contentLength = _content.size(); - - if (strlen(contentType) == 0) { + if (*contentType == '\0') { _setContentTypeFromPath(path); } else { _contentType = contentType; } - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26 + path.length() - filenameStart]; - char *filename = (char *)path.c_str() + filenameStart; - if (download) { - // set filename and force download - snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); + // Extract filename from path and set as download attachment + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26 + path.length() - filenameStart]; + char *filename = (char *)path.c_str() + filenameStart; + snprintf(buf, sizeof(buf), T_attachment, filename); + addHeader(T_Content_Disposition, buf, false); } else { - // set filename and force rendering - snprintf_P(buf, sizeof(buf), PSTR("inline")); + // Serve file inline (display in browser) + addHeader(T_Content_Disposition, T_inline, false); } - addHeader(T_Content_Disposition, buf, false); + + _code = 200; } AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { _code = 200; - _path = path; - if (!download && String(content.name()).endsWith(T__gz) && !path.endsWith(T__gz)) { + if (String(content.name()).endsWith(T__gz) && !path.endsWith(T__gz)) { addHeader(T_Content_Encoding, T_gzip, false); _callback = nullptr; // Unable to process gzipped templates _sendContentLength = true; @@ -822,7 +893,7 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS _contentType = contentType; // internal buffer will be null on allocation failure _content = std::unique_ptr(new cbuf(bufferSize)); - if (_content->size() != bufferSize) { + if (bufferSize && _content->size() < bufferSize) { #ifdef ESP32 log_e("Failed to allocate"); #endif @@ -840,6 +911,14 @@ size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { if (len > _content->room()) { size_t needed = len - _content->room(); _content->resizeAdd(needed); + // log a warning if allocation failed, but do not return: keep writing the bytes we can + // with _content->write: if len is more than the available size in the buffer, only + // the available size will be written + if (len > _content->room()) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + } } size_t written = _content->write((const char *)data, len); _contentLength += written; diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 7fc54bf..c3c3ee7 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -4,10 +4,18 @@ #include "ESPAsyncWebServer.h" #include "WebHandlerImpl.h" +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) +#include +#elif defined(ESP8266) +#include +#else +#error Platform not supported +#endif + using namespace asyncsrv; bool ON_STA_FILTER(AsyncWebServerRequest *request) { -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI return WiFi.localIP() == request->client()->localIP(); #else return false; @@ -15,7 +23,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) { } bool ON_AP_FILTER(AsyncWebServerRequest *request) { -#ifndef CONFIG_IDF_TARGET_ESP32H2 +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI return WiFi.localIP() != request->client()->localIP(); #else return false; diff --git a/src/literals.h b/src/literals.h index a69f78b..fc0fff1 100644 --- a/src/literals.h +++ b/src/literals.h @@ -10,39 +10,39 @@ static constexpr const char *empty = ""; static constexpr const char *T__opaque = "\", opaque=\""; static constexpr const char *T_100_CONTINUE = "100-continue"; static constexpr const char *T_13 = "13"; -static constexpr const char *T_ACCEPT = "accept"; -static constexpr const char *T_Accept_Ranges = "accept-ranges"; -static constexpr const char *T_app_xform_urlencoded = "application/x-www-form-urlencoded"; -static constexpr const char *T_AUTH = "authorization"; +static constexpr const char *T_ACCEPT = "Accept"; +static constexpr const char *T_Accept_Ranges = "Accept-Ranges"; +static constexpr const char *T_attachment = "attachment; filename=\"%s\""; +static constexpr const char *T_AUTH = "Authorization"; static constexpr const char *T_auth_nonce = "\", qop=\"auth\", nonce=\""; -static constexpr const char *T_BASIC = "basic"; -static constexpr const char *T_BASIC_REALM = "basic realm=\""; -static constexpr const char *T_BEARER = "bearer"; +static constexpr const char *T_BASIC = "Basic"; +static constexpr const char *T_BASIC_REALM = "Basic realm=\""; +static constexpr const char *T_BEARER = "Bearer"; static constexpr const char *T_BODY = "body"; -static constexpr const char *T_Cache_Control = "cache-control"; +static constexpr const char *T_Cache_Control = "Cache-Control"; static constexpr const char *T_chunked = "chunked"; static constexpr const char *T_close = "close"; static constexpr const char *T_cnonce = "cnonce"; -static constexpr const char *T_Connection = "connection"; -static constexpr const char *T_Content_Disposition = "content-disposition"; -static constexpr const char *T_Content_Encoding = "content-encoding"; -static constexpr const char *T_Content_Length = "content-length"; -static constexpr const char *T_Content_Type = "content-type"; -static constexpr const char *T_Content_Location = "content-location"; -static constexpr const char *T_Cookie = "cookie"; -static constexpr const char *T_CORS_ACAC = "access-control-allow-credentials"; -static constexpr const char *T_CORS_ACAH = "access-control-allow-headers"; -static constexpr const char *T_CORS_ACAM = "access-control-allow-methods"; -static constexpr const char *T_CORS_ACAO = "access-control-allow-origin"; -static constexpr const char *T_CORS_ACMA = "access-control-max-age"; -static constexpr const char *T_CORS_O = "origin"; +static constexpr const char *T_Connection = "Connection"; +static constexpr const char *T_Content_Disposition = "Content-Disposition"; +static constexpr const char *T_Content_Encoding = "Content-Encoding"; +static constexpr const char *T_Content_Length = "Content-Length"; +static constexpr const char *T_Content_Type = "Content-Type"; +static constexpr const char *T_Content_Location = "Content-Location"; +static constexpr const char *T_Cookie = "Cookie"; +static constexpr const char *T_CORS_ACAC = "Access-Control-Allow-Credentials"; +static constexpr const char *T_CORS_ACAH = "Access-Control-Allow-Headers"; +static constexpr const char *T_CORS_ACAM = "Access-Control-Allow-Methods"; +static constexpr const char *T_CORS_ACAO = "Access-Control-Allow-Origin"; +static constexpr const char *T_CORS_ACMA = "Access-Control-Max-Age"; +static constexpr const char *T_CORS_O = "Origin"; static constexpr const char *T_data_ = "data: "; -static constexpr const char *T_Date = "date"; -static constexpr const char *T_DIGEST = "digest"; -static constexpr const char *T_DIGEST_ = "digest "; -static constexpr const char *T_ETag = "etag"; +static constexpr const char *T_Date = "Date"; +static constexpr const char *T_DIGEST = "Digest"; +static constexpr const char *T_DIGEST_ = "Digest "; +static constexpr const char *T_ETag = "ETag"; static constexpr const char *T_event_ = "event: "; -static constexpr const char *T_EXPECT = "expect"; +static constexpr const char *T_EXPECT = "Expect"; static constexpr const char *T_FALSE = "false"; static constexpr const char *T_filename = "filename"; static constexpr const char *T_gzip = "gzip"; @@ -50,12 +50,13 @@ static constexpr const char *T_Host = "host"; static constexpr const char *T_HTTP_1_0 = "HTTP/1.0"; static constexpr const char *T_HTTP_100_CONT = "HTTP/1.1 100 Continue\r\n\r\n"; static constexpr const char *T_id__ = "id: "; -static constexpr const char *T_IMS = "if-modified-since"; -static constexpr const char *T_INM = "if-none-match"; +static constexpr const char *T_IMS = "If-Modified-Since"; +static constexpr const char *T_INM = "If-None-Match"; +static constexpr const char *T_inline = "inline"; static constexpr const char *T_keep_alive = "keep-alive"; -static constexpr const char *T_Last_Event_ID = "last-event-id"; -static constexpr const char *T_Last_Modified = "last-modified"; -static constexpr const char *T_LOCATION = "location"; +static constexpr const char *T_Last_Event_ID = "Last-Event-ID"; +static constexpr const char *T_Last_Modified = "Last-Modified"; +static constexpr const char *T_LOCATION = "Location"; static constexpr const char *T_LOGIN_REQ = "Login Required"; static constexpr const char *T_MULTIPART_ = "multipart/"; static constexpr const char *T_name = "name"; @@ -69,21 +70,20 @@ static constexpr const char *T_realm = "realm"; static constexpr const char *T_realm__ = "realm=\""; static constexpr const char *T_response = "response"; static constexpr const char *T_retry_ = "retry: "; -static constexpr const char *T_retry_after = "retry-after"; +static constexpr const char *T_retry_after = "Retry-After"; static constexpr const char *T_nn = "\n\n"; static constexpr const char *T_rn = "\r\n"; static constexpr const char *T_rnrn = "\r\n\r\n"; -static constexpr const char *T_Server = "server"; -static constexpr const char *T_Transfer_Encoding = "transfer-encoding"; +static constexpr const char *T_Server = "Server"; +static constexpr const char *T_Transfer_Encoding = "Transfer-Encoding"; static constexpr const char *T_TRUE = "true"; -static constexpr const char *T_UPGRADE = "upgrade"; +static constexpr const char *T_UPGRADE = "Upgrade"; static constexpr const char *T_uri = "uri"; static constexpr const char *T_username = "username"; static constexpr const char *T_WS = "websocket"; -static constexpr const char *T_WWW_AUTH = "www-authenticate"; +static constexpr const char *T_WWW_AUTH = "WWW-Authenticate"; // HTTP Methods - static constexpr const char *T_ANY = "ANY"; static constexpr const char *T_GET = "GET"; static constexpr const char *T_POST = "POST"; @@ -103,44 +103,55 @@ static constexpr const char *T_RCT_EVENT = "RCT_EVENT"; static constexpr const char *T_ERROR = "ERROR"; // extensions & MIME-Types -static constexpr const char *T__css = ".css"; -static constexpr const char *T__eot = ".eot"; -static constexpr const char *T__gif = ".gif"; -static constexpr const char *T__gz = ".gz"; -static constexpr const char *T__htm = ".htm"; -static constexpr const char *T__html = ".html"; -static constexpr const char *T__ico = ".ico"; -static constexpr const char *T__jpg = ".jpg"; -static constexpr const char *T__js = ".js"; -static constexpr const char *T__json = ".json"; -static constexpr const char *T__pdf = ".pdf"; -static constexpr const char *T__png = ".png"; -static constexpr const char *T__svg = ".svg"; -static constexpr const char *T__ttf = ".ttf"; -static constexpr const char *T__woff = ".woff"; -static constexpr const char *T__woff2 = ".woff2"; -static constexpr const char *T__xml = ".xml"; -static constexpr const char *T__zip = ".zip"; -static constexpr const char *T_application_javascript = "application/javascript"; +static constexpr const char *T__avif = ".avif"; // AVIF: Highly compressed images. Compatible with all modern browsers. +static constexpr const char *T__csv = ".csv"; // CSV: Data logging and configuration +static constexpr const char *T__css = ".css"; // CSS: Styling for web interfaces +static constexpr const char *T__gif = ".gif"; // GIF: Simple animations. Legacy support +static constexpr const char *T__gz = ".gz"; // GZ: compressed files +static constexpr const char *T__htm = ".htm"; // HTM: Web interface files +static constexpr const char *T__html = ".html"; // HTML: Web interface files +static constexpr const char *T__ico = ".ico"; // ICO: Favicons, system icons. Legacy support +static constexpr const char *T__jpg = ".jpg"; // JPEG/JPG: Photos. Legacy support +static constexpr const char *T__js = ".js"; // JavaScript: Interactive functionality +static constexpr const char *T__json = ".json"; // JSON: Data exchange format +static constexpr const char *T__mp4 = ".mp4"; // MP4: Proprietary format. Worse compression than WEBM. +static constexpr const char *T__opus = ".opus"; // OPUS: High compression audio format +static constexpr const char *T__pdf = ".pdf"; // PDF: Universal document format +static constexpr const char *T__png = ".png"; // PNG: Icons, logos, transparency. Legacy support +static constexpr const char *T__svg = ".svg"; // SVG: Vector graphics, icons (scalable, tiny file sizes) +static constexpr const char *T__ttf = ".ttf"; // TTF: Font file. Legacy support +static constexpr const char *T__txt = ".txt"; // TXT: Plain text files +static constexpr const char *T__webm = ".webm"; // WebM: Video. Open source, optimized for web. Compatible with all modern browsers. +static constexpr const char *T__webp = ".webp"; // WebP: Highly compressed images. Compatible with all modern browsers. +static constexpr const char *T__woff = ".woff"; // WOFF: Font file. Legacy support +static constexpr const char *T__woff2 = ".woff2"; // WOFF2: Better compression. Compatible with all modern browsers. +static constexpr const char *T__xml = ".xml"; // XML: Configuration and data files +static constexpr const char *T_application_javascript = "application/javascript"; // Obsolete type for JavaScript static constexpr const char *T_application_json = "application/json"; static constexpr const char *T_application_msgpack = "application/msgpack"; +static constexpr const char *T_application_octet_stream = "application/octet-stream"; static constexpr const char *T_application_pdf = "application/pdf"; -static constexpr const char *T_application_x_gzip = "application/x-gzip"; -static constexpr const char *T_application_zip = "application/zip"; -static constexpr const char *T_font_eot = "font/eot"; +static constexpr const char *T_app_xform_urlencoded = "application/x-www-form-urlencoded"; +static constexpr const char *T_audio_opus = "audio/opus"; static constexpr const char *T_font_ttf = "font/ttf"; static constexpr const char *T_font_woff = "font/woff"; static constexpr const char *T_font_woff2 = "font/woff2"; +static constexpr const char *T_image_avif = "image/avif"; static constexpr const char *T_image_gif = "image/gif"; static constexpr const char *T_image_jpeg = "image/jpeg"; static constexpr const char *T_image_png = "image/png"; static constexpr const char *T_image_svg_xml = "image/svg+xml"; +static constexpr const char *T_image_webp = "image/webp"; static constexpr const char *T_image_x_icon = "image/x-icon"; static constexpr const char *T_text_css = "text/css"; +static constexpr const char *T_text_csv = "text/csv"; static constexpr const char *T_text_event_stream = "text/event-stream"; static constexpr const char *T_text_html = "text/html"; +static constexpr const char *T_text_javascript = "text/javascript"; static constexpr const char *T_text_plain = "text/plain"; static constexpr const char *T_text_xml = "text/xml"; +static constexpr const char *T_video_mp4 = "video/mp4"; +static constexpr const char *T_video_webm = "video/webm"; // Response codes static constexpr const char *T_HTTP_CODE_100 = "Continue"; @@ -175,7 +186,7 @@ static constexpr const char *T_HTTP_CODE_412 = "Precondition Failed"; static constexpr const char *T_HTTP_CODE_413 = "Request Entity Too Large"; static constexpr const char *T_HTTP_CODE_414 = "Request-URI Too Large"; static constexpr const char *T_HTTP_CODE_415 = "Unsupported Media Type"; -static constexpr const char *T_HTTP_CODE_416 = "Requested range not satisfiable"; +static constexpr const char *T_HTTP_CODE_416 = "Requested Range Not Satisfiable"; static constexpr const char *T_HTTP_CODE_417 = "Expectation Failed"; static constexpr const char *T_HTTP_CODE_429 = "Too Many Requests"; static constexpr const char *T_HTTP_CODE_500 = "Internal Server Error"; @@ -183,11 +194,14 @@ static constexpr const char *T_HTTP_CODE_501 = "Not Implemented"; static constexpr const char *T_HTTP_CODE_502 = "Bad Gateway"; static constexpr const char *T_HTTP_CODE_503 = "Service Unavailable"; static constexpr const char *T_HTTP_CODE_504 = "Gateway Time-out"; -static constexpr const char *T_HTTP_CODE_505 = "HTTP Version not supported"; +static constexpr const char *T_HTTP_CODE_505 = "HTTP Version Not Supported"; static constexpr const char *T_HTTP_CODE_ANY = "Unknown code"; -static constexpr const uint8_t T_only_once_headers_len = 11; -static constexpr const char *T_only_once_headers[] = {T_Content_Length, T_Content_Type, T_Date, T_ETag, T_Last_Modified, T_LOCATION, T_retry_after, - T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH}; +static constexpr const char *T_only_once_headers[] = { + T_Accept_Ranges, T_Content_Length, T_Content_Type, T_Connection, T_CORS_ACAC, T_CORS_ACAH, T_CORS_ACAM, T_CORS_ACAO, + T_CORS_ACMA, T_CORS_O, T_Date, T_DIGEST, T_ETag, T_Last_Modified, T_LOCATION, T_retry_after, + T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH +}; +static constexpr size_t T_only_once_headers_len = sizeof(T_only_once_headers) / sizeof(T_only_once_headers[0]); } // namespace asyncsrv