Update to version 3.9.6
This commit is contained in:
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository
|
||||
blank_issues_enabled: false
|
||||
91
.github/ISSUE_TEMPLATE/issue-report.yml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/issue-report.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: 🐛 Bug Report
|
||||
description: File a bug report
|
||||
labels: ["Status: Awaiting triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
### ⚠️ Please remember: issues are for *bugs* only!
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### Unsure? Have a questions? 👉 [Start a new discussion](https://github.com/ESP32Async/ESPAsyncWebServer/discussions/new)
|
||||
|
||||
#### Before opening a new issue, please make sure you have searched:
|
||||
|
||||
- In the [documentation](https://github.com/ESP32Async/ESPAsyncWebServer)
|
||||
- In the [discussions](https://github.com/ESP32Async/ESPAsyncWebServer/discussions)
|
||||
- In the [issues](https://github.com/ESP32Async/ESPAsyncWebServer/issues)
|
||||
- In the [examples](https://github.com/ESP32Async/ESPAsyncWebServer/tree/main/examples)
|
||||
|
||||
#### Make sure you are using:
|
||||
|
||||
- The [latest version of ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer/releases)
|
||||
- The [latest version of AsyncTCP](https://github.com/ESP32Async/AsyncTCP/releases) (for ESP32)
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
options:
|
||||
- ESP32
|
||||
- ESP8266
|
||||
- RP2040
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: tooling
|
||||
attributes:
|
||||
label: IDE / Tooling
|
||||
options:
|
||||
- Arduino (IDE/CLI)
|
||||
- pioarduino
|
||||
- ESP-IDF
|
||||
- PlatformIO
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear and concise description of the issue.
|
||||
placeholder: A clear and concise description of the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: stack-trace
|
||||
attributes:
|
||||
label: Stack Trace
|
||||
description: Please provide a debug message or error message. If you have a Guru Meditation Error or Backtrace, [please decode it](https://maximeborges.github.io/esp-stacktrace-decoder/).
|
||||
placeholder: For Arduino IDE, Enable Core debug level - Debug on tools menu, then put the serial output here.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: Minimal Reproductible Example (MRE)
|
||||
description: Post the code or the steps to reproduce the issue.
|
||||
placeholder: Post the code or the steps to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: confirmation
|
||||
attributes:
|
||||
label: "I confirm that:"
|
||||
options:
|
||||
- label: I have read the documentation.
|
||||
required: true
|
||||
- label: I have searched for similar discussions.
|
||||
required: true
|
||||
- label: I have searched for similar issues.
|
||||
required: true
|
||||
- label: I have looked at the examples.
|
||||
required: true
|
||||
- label: I have upgraded to the lasted version of ESPAsyncWebServer (and AsyncTCP for ESP32).
|
||||
required: true
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Set update schedule for GitHub Actions
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
40
.github/scripts/update-version.sh
vendored
Executable file
40
.github/scripts/update-version.sh
vendored
Executable file
@@ -0,0 +1,40 @@
|
||||
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2002
|
||||
|
||||
# fail the script if any command unexpectedly fails
|
||||
set -e
|
||||
|
||||
if [ ! $# -eq 3 ]; then
|
||||
echo "Bad number of arguments: $#" >&2
|
||||
echo "usage: $0 <major> <minor> <patch>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
re='^[0-9]+$'
|
||||
if [[ ! $1 =~ $re ]] || [[ ! $2 =~ $re ]] || [[ ! $3 =~ $re ]] ; then
|
||||
echo "error: Not a valid version: $1.$2.$3" >&2
|
||||
echo "usage: $0 <major> <minor> <patch>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ASYNCWEBSERVER_VERSION_MAJOR="$1"
|
||||
ASYNCWEBSERVER_VERSION_MINOR="$2"
|
||||
ASYNCWEBSERVER_VERSION_PATCH="$3"
|
||||
ASYNCWEBSERVER_VERSION="$ASYNCWEBSERVER_VERSION_MAJOR.$ASYNCWEBSERVER_VERSION_MINOR.$ASYNCWEBSERVER_VERSION_PATCH"
|
||||
|
||||
echo "New AsyncTCP version: $ASYNCWEBSERVER_VERSION"
|
||||
|
||||
echo "Updating library.properties..."
|
||||
cat library.properties | sed "s/version=.*/version=$ASYNCWEBSERVER_VERSION/g" > __library.properties && mv __library.properties library.properties
|
||||
|
||||
echo "Updating library.json..."
|
||||
cat library.json | sed "s/^ \"version\":.*/ \"version\": \"$ASYNCWEBSERVER_VERSION\",/g" > __library.json && mv __library.json library.json
|
||||
|
||||
echo "Updating src/AsyncWebServerVersion.h..."
|
||||
cat src/AsyncWebServerVersion.h | \
|
||||
sed "s/#define ASYNCWEBSERVER_VERSION_MAJOR.*/#define ASYNCWEBSERVER_VERSION_MAJOR $ASYNCWEBSERVER_VERSION_MAJOR/g" | \
|
||||
sed "s/#define ASYNCWEBSERVER_VERSION_MINOR.*/#define ASYNCWEBSERVER_VERSION_MINOR $ASYNCWEBSERVER_VERSION_MINOR/g" | \
|
||||
sed "s/#define ASYNCWEBSERVER_VERSION_PATCH.*/#define ASYNCWEBSERVER_VERSION_PATCH $ASYNCWEBSERVER_VERSION_PATCH/g" > src/__AsyncWebServerVersion.h && mv src/__AsyncWebServerVersion.h src/AsyncWebServerVersion.h
|
||||
|
||||
exit 0
|
||||
25
.github/workflows/arduino-lint.yml
vendored
Normal file
25
.github/workflows/arduino-lint.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Arduino Lint
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
arduino-lint:
|
||||
name: Arduino Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: arduino/arduino-lint-action@v2
|
||||
with:
|
||||
library-manager: update
|
||||
194
.github/workflows/build-esp32.yml
vendored
Normal file
194
.github/workflows/build-esp32.yml
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Build (ESP32)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
arduino-esp32:
|
||||
name: ESP32 (arduino-cli) - Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install arduino-cli
|
||||
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
|
||||
|
||||
- name: Update core index
|
||||
run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
|
||||
|
||||
- name: Install core
|
||||
run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json esp32:esp32
|
||||
|
||||
- name: Install ArduinoJson
|
||||
run: arduino-cli lib install ArduinoJson
|
||||
|
||||
- name: Install AsyncTCP (ESP32)
|
||||
run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino"
|
||||
done
|
||||
|
||||
arduino-esp32-dev:
|
||||
name: ESP32 (arduino-cli) - Dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install arduino-cli
|
||||
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
|
||||
|
||||
- name: Update core index
|
||||
run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json
|
||||
|
||||
- name: Install core
|
||||
run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json esp32:esp32
|
||||
|
||||
- name: Install ArduinoJson
|
||||
run: arduino-cli lib install ArduinoJson
|
||||
|
||||
- name: Install AsyncTCP (ESP32)
|
||||
run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino"
|
||||
done
|
||||
|
||||
platformio-esp32-arduino2:
|
||||
name: ESP32 (pio) - Arduino 2
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- esp32dev
|
||||
- esp32-s2-saola-1
|
||||
- esp32-s3-devkitc-1
|
||||
- esp32-c3-devkitc-02
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-2
|
||||
done
|
||||
|
||||
platformio-esp32-arduino-3:
|
||||
name: ESP32 (pio) - Arduino 3
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- esp32dev
|
||||
- esp32-s2-saola-1
|
||||
- esp32-s3-devkitc-1
|
||||
- esp32-c3-devkitc-02
|
||||
- esp32-c6-devkitc-1
|
||||
- esp32-h2-devkitm-1
|
||||
- esp32-p4
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3
|
||||
done
|
||||
|
||||
platformio-specific-envs:
|
||||
name: ESP32 (pio) - Specific Envs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
env:
|
||||
- ci-latest-asynctcp
|
||||
- ci-no-json
|
||||
- ci-no-chunk-inflight
|
||||
- ci-arduino-2-esp-idf-log
|
||||
- ci-arduino-3-esp-idf-log
|
||||
- ci-regex
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=esp32dev pio run -e ${{ matrix.env }}
|
||||
done
|
||||
82
.github/workflows/build-esp8266.yml
vendored
Normal file
82
.github/workflows/build-esp8266.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Build (8266)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
arduino-esp8266:
|
||||
name: ESP8266 (arduino-cli)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install arduino-cli
|
||||
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
|
||||
|
||||
- name: Update core index
|
||||
run: arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json
|
||||
|
||||
- name: Install core
|
||||
run: arduino-cli core install --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266
|
||||
|
||||
- name: Install ArduinoJson
|
||||
run: arduino-cli lib install ArduinoJson
|
||||
|
||||
- name: Install ESPAsyncTCP (ESP8266)
|
||||
run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncTCP#v2.0.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
arduino-cli compile --library . --warnings none -b esp8266:esp8266:huzzah "examples/$i/$i.ino"
|
||||
done
|
||||
|
||||
platformio-esp8266:
|
||||
name: ESP8266 (pio)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- huzzah
|
||||
- d1_mini
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-esp8266
|
||||
done
|
||||
51
.github/workflows/build-libretiny.yml
vendored
Normal file
51
.github/workflows/build-libretiny.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Build (LibreTiny)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
platformio-libretiny:
|
||||
name: LibreTiny (pio)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- generic-bk7231n-qfn32-tuya
|
||||
- generic-rtl8710bn-2mb-788k
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in AsyncResponseStream Auth Headers; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-libretiny
|
||||
done
|
||||
89
.github/workflows/build-rpi.yml
vendored
Normal file
89
.github/workflows/build-rpi.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Build (RPI)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
arduino-rpi:
|
||||
name: RPI (arduino-cli)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- rpipicow
|
||||
- rpipico2w
|
||||
|
||||
steps:
|
||||
- name: Install arduino-cli
|
||||
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
|
||||
|
||||
- name: Update core index
|
||||
run: arduino-cli core update-index --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json
|
||||
|
||||
- name: Install core
|
||||
run: arduino-cli core install --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json rp2040:rp2040
|
||||
|
||||
- name: Install ArduinoJson
|
||||
run: arduino-cli lib install ArduinoJson
|
||||
|
||||
- name: Install RPAsyncTCP
|
||||
run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ayushsharma82/RPAsyncTCP#v1.3.2
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
arduino-cli compile --library . --warnings none -b rp2040:rp2040:${{ matrix.board }} "examples/$i/$i.ino"
|
||||
done
|
||||
|
||||
platformio-rpi:
|
||||
name: RPI (pio)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
board:
|
||||
- rpipicow
|
||||
- rpipico2w
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
- name: Install platformio
|
||||
run: |
|
||||
uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
|
||||
|
||||
- name: Build Examples
|
||||
run: |
|
||||
for i in `ls examples`; do
|
||||
echo "============================================================="
|
||||
echo "Building examples/$i..."
|
||||
echo "============================================================="
|
||||
PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-raspberrypi
|
||||
done
|
||||
46
.github/workflows/cpplint.yml
vendored
Normal file
46
.github/workflows/cpplint.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Cpplint
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cpplint:
|
||||
name: cpplint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ runner.os }}-cpplint
|
||||
path: ~/.cache/pip
|
||||
|
||||
- name: Pyhton
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: cpplint
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade cpplint
|
||||
cpplint \
|
||||
--repository=. \
|
||||
--recursive \
|
||||
--filter=-build/c++11,-build/namespaces,-readability/braces,-readability/casting,-readability/todo,-runtime/explicit,-runtime/indentation_namespace,-runtime/int,-runtime/references,-whitespace/blank_line,,-whitespace/braces,-whitespace/comments,-whitespace/indent,-whitespace/line_length,-whitespace/newline,-whitespace/parens \
|
||||
lib \
|
||||
include \
|
||||
src
|
||||
64
.github/workflows/pre-commit-status.yml
vendored
Normal file
64
.github/workflows/pre-commit-status.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# This needs to be in a separate workflow because it requires higher permissions than the calling workflow
|
||||
name: Report Pre-commit Check Status
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Pre-commit hooks]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
report-success:
|
||||
name: Report pre-commit success
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Report success
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = '${{ github.repository_owner }}';
|
||||
const repo = '${{ github.repository }}'.split('/')[1];
|
||||
const sha = '${{ github.event.workflow_run.head_sha }}';
|
||||
core.debug(`owner: ${owner}`);
|
||||
core.debug(`repo: ${repo}`);
|
||||
core.debug(`sha: ${sha}`);
|
||||
const { context: name, state } = (await github.rest.repos.createCommitStatus({
|
||||
context: 'Pre-commit checks',
|
||||
description: 'Pre-commit checks successful',
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
sha: sha,
|
||||
state: 'success',
|
||||
target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}'
|
||||
})).data;
|
||||
core.info(`${name} is ${state}`);
|
||||
|
||||
report-pending:
|
||||
name: Report pre-commit pending
|
||||
if: github.event.workflow_run.conclusion != 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Report pending
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const owner = '${{ github.repository_owner }}';
|
||||
const repo = '${{ github.repository }}'.split('/')[1];
|
||||
const sha = '${{ github.event.workflow_run.head_sha }}';
|
||||
core.debug(`owner: ${owner}`);
|
||||
core.debug(`repo: ${repo}`);
|
||||
core.debug(`sha: ${sha}`);
|
||||
const { context: name, state } = (await github.rest.repos.createCommitStatus({
|
||||
context: 'Pre-commit checks',
|
||||
description: 'The pre-commit checks need to be successful before merging',
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
sha: sha,
|
||||
state: 'pending',
|
||||
target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}'
|
||||
})).data;
|
||||
core.info(`${name} is ${state}`);
|
||||
80
.github/workflows/pre-commit.yml
vendored
Normal file
80
.github/workflows/pre-commit.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Pre-commit hooks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, labeled]
|
||||
|
||||
concurrency:
|
||||
group: pre-commit-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
if: |
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'Status: Pending Merge') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit')
|
||||
|
||||
name: Check if fixes are needed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout latest commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Remove Label
|
||||
if: contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit')
|
||||
run: gh pr edit ${{ github.event.number }} --remove-label 'Re-trigger Pre-commit'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
cache-dependency-path: pre-commit.requirements.txt
|
||||
cache: "pip"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Get Python version hash
|
||||
run: |
|
||||
echo "Using $(python -VV)"
|
||||
echo "PY_HASH=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Restore pre-commit cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pre-commit
|
||||
key: pre-commit-${{ env.PY_HASH }}-${{ hashFiles('.pre-commit-config.yaml', '.github/workflows/pre-commit.yml', 'pre-commit.requirements.txt') }}
|
||||
|
||||
- name: Install python dependencies
|
||||
run: python -m pip install -r pre-commit.requirements.txt
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v42.0.2
|
||||
|
||||
- name: Run pre-commit hooks in changed files
|
||||
run: pre-commit run --color=always --show-diff-on-failure --files ${{ steps.changed-files.outputs.all_changed_files }}
|
||||
|
||||
- name: Save pre-commit cache
|
||||
uses: actions/cache/save@v4
|
||||
if: ${{ always() && steps.restore-cache.outputs.cache-hit != 'true' }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pre-commit
|
||||
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
||||
|
||||
- name: Push changes using pre-commit-ci-lite
|
||||
uses: pre-commit-ci/lite-action@v1.1.0
|
||||
# Only push changes in PRs
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
with:
|
||||
msg: "ci(pre-commit): Apply automatic fixes"
|
||||
62
.github/workflows/publish-pio-registry.yml
vendored
Normal file
62
.github/workflows/publish-pio-registry.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: Publish to PlatformIO
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Tag name of the release to publish (use 'latest' for the most recent tag)"
|
||||
required: true
|
||||
default: "latest"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0 # Ensure all commits and tags are fetched
|
||||
|
||||
- name: Show latest tag
|
||||
run: git tag --sort=-creatordate | head -n 1
|
||||
|
||||
- name: Download release zip
|
||||
run: |
|
||||
if [ "${{ inputs.tag_name }}" == "latest" ]; then
|
||||
TAG_NAME=$(git tag --sort=-creatordate | head -n 1)
|
||||
if [ -z "$TAG_NAME" ]; then
|
||||
echo "Error: No tags found in the repository."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
TAG_NAME="${{ inputs.tag_name }}"
|
||||
fi
|
||||
echo "Downloading tag: $TAG_NAME"
|
||||
curl -L -o project.zip "https://github.com/${{ github.repository }}/archive/refs/tags/$TAG_NAME.zip"
|
||||
|
||||
# - name: Cache PlatformIO
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# key: ${{ runner.os }}-pio
|
||||
# path: |
|
||||
# ~/.cache/pip
|
||||
# ~/.platformio
|
||||
|
||||
- name: Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install PlatformIO CLI
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade platformio
|
||||
|
||||
- name: Publish to PlatformIO
|
||||
run: pio pkg publish --no-interactive --owner ${{ github.repository_owner }} project.zip
|
||||
env:
|
||||
PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }}
|
||||
24
.github/workflows/stale.yaml
vendored
Normal file
24
.github/workflows/stale.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days."
|
||||
stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days."
|
||||
close-issue-message: "This issue was closed because it has been stalled for 7 days with no activity."
|
||||
close-pr-message: "This PR was closed because it has been stalled for 7 days with no activity."
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 30
|
||||
days-before-issue-close: 7
|
||||
days-before-pr-close: 7
|
||||
|
||||
permissions:
|
||||
# contents: write # only for delete-branch option
|
||||
issues: write
|
||||
pull-requests: write
|
||||
50
.github/workflows/upload-idf-component.yml
vendored
Normal file
50
.github/workflows/upload-idf-component.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Publish ESP-IDF Component
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Component version (1.2.3, 1.2.3-rc1 or 1.2.3.4)'
|
||||
required: true
|
||||
git_ref:
|
||||
description: 'Git ref with the source (branch, tag or commit)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload_components:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the release tag
|
||||
env:
|
||||
head_branch: ${{ inputs.tag || github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
# Read and sanitize the branch/tag name
|
||||
branch=$(echo "$head_branch" | tr -cd '[:alnum:]/_.-')
|
||||
|
||||
if [[ $branch == refs/tags/* ]]; then
|
||||
tag="${branch#refs/tags/}"
|
||||
elif [[ $branch =~ ^[v]*[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then
|
||||
tag=$branch
|
||||
else
|
||||
echo "Tag not found in $branch. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tag: $tag"
|
||||
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.git_ref || env.RELEASE_TAG }}
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Upload components to the component registry
|
||||
uses: espressif/upload-components-ci-action@v1
|
||||
with:
|
||||
name: espasyncwebserver
|
||||
version: ${{ env.RELEASE_TAG }}
|
||||
namespace: esp32async
|
||||
api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }}
|
||||
48
data/README.md
Normal file
48
data/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <DNSServer.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
@@ -20,7 +20,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to trigger an async client request from a browser request and send the client response back to the browser through websocket
|
||||
@@ -70,7 +70,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Authentication and authorization middlewares
|
||||
@@ -29,30 +29,36 @@ static AsyncAuthenticationMiddleware basicAuthHash;
|
||||
static AsyncAuthenticationMiddleware digestAuth;
|
||||
static AsyncAuthenticationMiddleware digestAuthHash;
|
||||
|
||||
static AsyncAuthenticationMiddleware bearerAuthSharedKey;
|
||||
static AsyncAuthenticationMiddleware bearerAuthJWT;
|
||||
|
||||
// complex authentication which adds request attributes for the next middlewares and handler
|
||||
static AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest *request, ArMiddlewareNext next) {
|
||||
if (!request->authenticate("user", "password")) {
|
||||
if (request->authenticate("Mathieu", "password")) {
|
||||
request->setAttribute("user", "Mathieu");
|
||||
} else if (request->authenticate("Bob", "password")) {
|
||||
request->setAttribute("user", "Bob");
|
||||
} else {
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
|
||||
// add attributes to the request for the next middlewares and handler
|
||||
request->setAttribute("user", "Mathieu");
|
||||
request->setAttribute("role", "staff");
|
||||
if (request->hasParam("token")) {
|
||||
request->setAttribute("token", request->getParam("token")->value().c_str());
|
||||
if (request->getAttribute("user") == "Mathieu") {
|
||||
request->setAttribute("role", "staff");
|
||||
} else {
|
||||
request->setAttribute("role", "user");
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
static AsyncAuthorizationMiddleware authz([](AsyncWebServerRequest *request) {
|
||||
return request->getAttribute("token") == "123";
|
||||
return request->getAttribute("role") == "staff";
|
||||
});
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -87,6 +93,36 @@ void setup() {
|
||||
digestAuthHash.setAuthFailureMessage("Authentication failed");
|
||||
digestAuthHash.setAuthType(AsyncAuthType::AUTH_DIGEST);
|
||||
|
||||
// bearer authentication with shared key
|
||||
bearerAuthSharedKey.setAuthType(AsyncAuthType::AUTH_BEARER);
|
||||
bearerAuthSharedKey.setToken("shared-secret-key");
|
||||
|
||||
// bearer authentication with a JWT token
|
||||
bearerAuthJWT.setAuthType(AsyncAuthType::AUTH_BEARER);
|
||||
bearerAuthJWT.setAuthentificationFunction([](AsyncWebServerRequest *request) {
|
||||
const String &token = request->authChallenge();
|
||||
// 1. decode base64 token
|
||||
// 2. decrypt token
|
||||
const String &decrypted = "..."; // TODO
|
||||
// 3. validate token (check signature, expiration, etc)
|
||||
bool valid = token == "<token>" || token == "<another token>";
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
// 4. extract user info from token and set request attributes
|
||||
if (token == "<token>") {
|
||||
request->setAttribute("user", "Mathieu");
|
||||
request->setAttribute("role", "staff");
|
||||
return true; // return true if token is valid, false otherwise
|
||||
}
|
||||
if (token == "<another token>") {
|
||||
request->setAttribute("user", "Bob");
|
||||
request->setAttribute("role", "user");
|
||||
return true; // return true if token is valid, false otherwise
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// basic authentication method
|
||||
// curl -v -u admin:admin http://192.168.4.1/auth-basic
|
||||
server
|
||||
@@ -132,9 +168,9 @@ void setup() {
|
||||
.addMiddleware(&digestAuthHash);
|
||||
|
||||
// test digest auth custom authorization middleware
|
||||
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=123 => OK
|
||||
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=456 => 403
|
||||
// curl -v --digest -u user:FAILED http://192.168.4.1/auth-custom?token=456 => 401
|
||||
// curl -v --digest -u Mathieu:password http://192.168.4.1/auth-custom => OK
|
||||
// curl -v --digest -u Bob:password http://192.168.4.1/auth-custom => 403
|
||||
// curl -v --digest -u any:password http://192.168.4.1/auth-custom => 401
|
||||
server
|
||||
.on(
|
||||
"/auth-custom", HTTP_GET,
|
||||
@@ -148,6 +184,32 @@ void setup() {
|
||||
)
|
||||
.addMiddlewares({&complexAuth, &authz});
|
||||
|
||||
// Bearer authentication with a shared key
|
||||
// curl -v -H "Authorization: Bearer shared-secret-key" http://192.168.4.1/auth-bearer-shared-key => OK
|
||||
server
|
||||
.on(
|
||||
"/auth-bearer-shared-key", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "Hello, world!");
|
||||
}
|
||||
)
|
||||
.addMiddleware(&bearerAuthSharedKey);
|
||||
|
||||
// Bearer authentication with a JWT token
|
||||
// curl -v -H "Authorization: Bearer <token>" http://192.168.4.1/auth-bearer-jwt => OK
|
||||
// curl -v -H "Authorization: Bearer <another token>" http://192.168.4.1/auth-bearer-jwt => 403 Forbidden
|
||||
// curl -v -H "Authorization: Bearer invalid-token" http://192.168.4.1/auth-bearer-jwt => 401 Unauthorized
|
||||
server
|
||||
.on(
|
||||
"/auth-bearer-jwt", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
Serial.println("User: " + request->getAttribute("user"));
|
||||
Serial.println("Role: " + request->getAttribute("role"));
|
||||
request->send(200, "text/plain", "Hello, world!");
|
||||
}
|
||||
)
|
||||
.addMiddlewares({&bearerAuthJWT, &authz});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// How to use CORS middleware
|
||||
@@ -25,7 +25,7 @@ static AsyncCorsMiddleware cors;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <DNSServer.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
|
||||
response->print("<p>This is our captive portal front page.</p>");
|
||||
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
|
||||
#endif
|
||||
response->print("</body></html>");
|
||||
@@ -41,7 +41,7 @@ void setup() {
|
||||
Serial.println();
|
||||
Serial.println("Configuring access point...");
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
if (!WiFi.softAP("esp-captive")) {
|
||||
Serial.println("Soft AP creation failed.");
|
||||
while (1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to catch all requests and send a 404 Not Found response
|
||||
@@ -86,7 +86,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Chunk response with caching example
|
||||
@@ -86,7 +86,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -94,11 +94,11 @@ void setup() {
|
||||
// first time: serves the file and cache headers
|
||||
// curl -N -v http://192.168.4.1/ --output -
|
||||
//
|
||||
// secodn time: serves 304
|
||||
// second time: serves 304
|
||||
// curl -N -v -H "if-none-match: 4272" http://192.168.4.1/ --output -
|
||||
//
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
String etag = String(htmlContentLength);
|
||||
String etag = "\"" + String(htmlContentLength) + "\""; // RFC9110: ETag must be enclosed in double quotes
|
||||
|
||||
if (request->header(asyncsrv::T_INM) == etag) {
|
||||
request->send(304);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to wait in a chunk response for incoming data
|
||||
@@ -19,12 +19,6 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static const char *htmlContent PROGMEM = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -96,7 +90,7 @@ static int key = -1;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -107,7 +101,7 @@ void setup() {
|
||||
|
||||
server.addMiddleware(&requestLogger);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
|
||||
//
|
||||
// HOW TO RUN THIS EXAMPLE:
|
||||
@@ -174,7 +168,7 @@ void setup() {
|
||||
return 0; // 0 means we are done
|
||||
}
|
||||
|
||||
// log_d("UART answered!");
|
||||
// async_ws_log_d("UART answered!");
|
||||
|
||||
String answer = "You typed: ";
|
||||
answer.concat((char)key);
|
||||
@@ -193,10 +187,10 @@ void setup() {
|
||||
},
|
||||
NULL, // upload handler is not used so it should be NULL
|
||||
[](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
|
||||
// log_d("Body: index: %u, len: %u, total: %u", index, len, total);
|
||||
// async_ws_log_d("Body: index: %u, len: %u, total: %u", index, len, total);
|
||||
|
||||
if (!index) {
|
||||
// log_d("Start body parsing");
|
||||
// async_ws_log_d("Start body parsing");
|
||||
request->_tempObject = new String();
|
||||
// cast request->_tempObject pointer to String and reserve total size
|
||||
((String *)request->_tempObject)->reserve(total);
|
||||
@@ -204,7 +198,7 @@ void setup() {
|
||||
request->client()->setRxTimeout(30);
|
||||
}
|
||||
|
||||
// log_d("Append body data");
|
||||
// async_ws_log_d("Append body data");
|
||||
((String *)request->_tempObject)->concat((const char *)data, len);
|
||||
}
|
||||
);
|
||||
@@ -217,13 +211,13 @@ void setup() {
|
||||
void loop() {
|
||||
if (triggerUART.length() && key == -1) {
|
||||
Serial.println(triggerUART);
|
||||
// log_d("Waiting for UART input...");
|
||||
// async_ws_log_d("Waiting for UART input...");
|
||||
while (!Serial.available()) {
|
||||
delay(100);
|
||||
}
|
||||
key = Serial.read();
|
||||
Serial.flush();
|
||||
// log_d("UART input: %c", key);
|
||||
// async_ws_log_d("UART input: %c", key);
|
||||
triggerUART = emptyString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// https://github.com/ESP32Async/ESPAsyncWebServer/discussions/23
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -39,6 +39,10 @@ void setup() {
|
||||
|
||||
Serial.println("end()");
|
||||
server.end();
|
||||
|
||||
Serial.println("waiting before restarting server...");
|
||||
delay(100);
|
||||
|
||||
server.begin();
|
||||
Serial.println("begin() - run: curl -v http://192.168.4.1/ => should succeed");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use setFilter to route requests to different handlers based on WiFi mode
|
||||
@@ -32,7 +32,7 @@ public:
|
||||
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
|
||||
response->print("<p>This is out captive portal front page.</p>");
|
||||
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
|
||||
#endif
|
||||
response->print("</body></html>");
|
||||
@@ -51,17 +51,17 @@ void setup() {
|
||||
"/", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
Serial.println("Captive portal request...");
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
|
||||
#endif
|
||||
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
|
||||
#endif
|
||||
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
|
||||
#endif
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
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...");
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
|
||||
#endif
|
||||
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
|
||||
#endif
|
||||
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
|
||||
#endif
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
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();
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.persistent(false);
|
||||
WiFi.begin("IoT");
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a large HTML page from flash memory without copying it to heap in a temporary buffer
|
||||
@@ -86,7 +86,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to manipulate headers in the request / response
|
||||
@@ -33,7 +33,7 @@ AsyncHeaderFreeMiddleware headerFree;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Query and send headers
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to send and receive Json data
|
||||
@@ -19,27 +19,21 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/json2");
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
//
|
||||
// sends JSON using AsyncJsonResponse
|
||||
//
|
||||
@@ -62,8 +56,8 @@ void setup() {
|
||||
JsonDocument doc;
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
root["foo"] = "bar";
|
||||
serializeJson(root, *response);
|
||||
Serial.println();
|
||||
// serializeJson(root, Serial);
|
||||
// Serial.println();
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
@@ -92,12 +86,38 @@ void setup() {
|
||||
});
|
||||
|
||||
server.addHandler(handler);
|
||||
|
||||
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
|
||||
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json3
|
||||
|
||||
server.on("/json3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request : ");
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
// not needed
|
||||
static uint32_t lastHeapTime = 0;
|
||||
static uint32_t lastHeap = 0;
|
||||
|
||||
void loop() {
|
||||
delay(100);
|
||||
#ifdef ESP32
|
||||
uint32_t now = millis();
|
||||
if (now - lastHeapTime >= 500) {
|
||||
uint32_t heap = ESP.getFreeHeap();
|
||||
if (heap != lastHeap) {
|
||||
lastHeap = heap;
|
||||
async_ws_log_w("Free heap: %" PRIu32, heap);
|
||||
}
|
||||
lastHeapTime = now;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
178
examples/LargeResponse/LargeResponse.ino
Normal file
178
examples/LargeResponse/LargeResponse.ino
Normal file
@@ -0,0 +1,178 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Example to send a large response and control the filling of the buffer.
|
||||
//
|
||||
// This is also a MRE for:
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <RPAsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
static const size_t totalResponseSize = 16 * 1000; // 16 KB
|
||||
static char fillChar = 'A';
|
||||
|
||||
class CustomResponse : public AsyncAbstractResponse {
|
||||
public:
|
||||
explicit CustomResponse() {
|
||||
_code = 200;
|
||||
_contentType = "text/plain";
|
||||
_sendContentLength = false;
|
||||
}
|
||||
|
||||
bool _sourceValid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
|
||||
if (_sent == RESPONSE_TRY_AGAIN) {
|
||||
Serial.println("Simulating temporary unavailability of data...");
|
||||
_sent = 0;
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
size_t remaining = totalResponseSize - _sent;
|
||||
if (remaining == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (buflen > remaining) {
|
||||
buflen = remaining;
|
||||
}
|
||||
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
|
||||
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
|
||||
_sent += buflen;
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return buflen;
|
||||
}
|
||||
|
||||
private:
|
||||
char fillChar = 'A';
|
||||
size_t _sent = 0;
|
||||
};
|
||||
|
||||
// Code to reproduce issues:
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
|
||||
//
|
||||
// https://github.com/ESP32Async/ESPAsyncWebServer/pull/317#issuecomment-3421141039
|
||||
//
|
||||
// I cracked it.
|
||||
// So this is how it works:
|
||||
// That space that _tcp is writing to identified by CONFIG_TCP_SND_BUF_DEFAULT (and is value-matching with default TCP windows size which is very confusing itself).
|
||||
// The space returned by client()->write() and client->space() somehow might not be atomically/thread synced (had not dived that deep yet). So if first call to _fillBuffer is done via user-code thread and ended up with some small amount of data consumed and second one is done by _poll or _ack? returns full size again! This is where old code fails.
|
||||
// If you change your class this way it will fail 100%.
|
||||
class CustomResponseMRE : public AsyncAbstractResponse {
|
||||
public:
|
||||
explicit CustomResponseMRE() {
|
||||
_code = 200;
|
||||
_contentType = "text/plain";
|
||||
_sendContentLength = false;
|
||||
// add some useless headers
|
||||
addHeader("Clear-Site-Data", "Clears browsing data (e.g., cookies, storage, cache) associated with the requesting website.");
|
||||
addHeader(
|
||||
"No-Vary-Search", "Specifies a set of rules that define how a URL's query parameters will affect cache matching. These rules dictate whether the same "
|
||||
"URL with different URL parameters should be saved as separate browser cache entries"
|
||||
);
|
||||
}
|
||||
|
||||
bool _sourceValid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
|
||||
if (fillChar == NULL) {
|
||||
fillChar = 'A';
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
if (_sent == RESPONSE_TRY_AGAIN) {
|
||||
Serial.println("Simulating temporary unavailability of data...");
|
||||
_sent = 0;
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
size_t remaining = totalResponseSize - _sent;
|
||||
if (remaining == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (buflen > remaining) {
|
||||
buflen = remaining;
|
||||
}
|
||||
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
|
||||
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
|
||||
_sent += buflen;
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return buflen;
|
||||
}
|
||||
|
||||
private:
|
||||
char fillChar = NULL;
|
||||
size_t _sent = 0;
|
||||
};
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// Example to use a AwsResponseFiller
|
||||
//
|
||||
// curl -v http://192.168.4.1/1 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
fillChar = 'A';
|
||||
AsyncWebServerResponse *response = request->beginResponse("text/plain", totalResponseSize, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
|
||||
size_t remaining = totalResponseSize - index;
|
||||
size_t toSend = (remaining < maxLen) ? remaining : maxLen;
|
||||
Serial.printf("Filling '%c' @ index: %u, maxLen: %u, toSend: %u\n", fillChar, index, maxLen, toSend);
|
||||
std::fill_n(buffer, toSend, static_cast<uint8_t>(fillChar));
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return toSend;
|
||||
});
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// Example to use a AsyncAbstractResponse
|
||||
//
|
||||
// curl -v http://192.168.4.1/2 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(new CustomResponse());
|
||||
});
|
||||
|
||||
// Example to use a AsyncAbstractResponse
|
||||
//
|
||||
// curl -v http://192.168.4.1/3 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/3", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(new CustomResponseMRE());
|
||||
});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to log the incoming request and response as a curl-like syntax
|
||||
@@ -25,7 +25,7 @@ static AsyncLoggingMiddleware requestLogger;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to send and receive Message Pack data
|
||||
@@ -19,27 +19,21 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
static AsyncCallbackMessagePackWebHandler *handler = new AsyncCallbackMessagePackWebHandler("/msgpack2");
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/msgpack2");
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
//
|
||||
// sends MessagePack using AsyncMessagePackResponse
|
||||
//
|
||||
@@ -57,18 +51,26 @@ void setup() {
|
||||
//
|
||||
// curl -v http://192.168.4.1/msgpack2
|
||||
//
|
||||
// Save file: curl -v http://192.168.4.1/msgpack2 -o msgpack.bin
|
||||
//
|
||||
server.on("/msgpack2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *response = request->beginResponseStream("application/msgpack");
|
||||
JsonDocument doc;
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
root["foo"] = "bar";
|
||||
root["name"] = "Bob";
|
||||
serializeMsgPack(root, *response);
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// POST file:
|
||||
//
|
||||
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack2
|
||||
//
|
||||
handler->setMethod(HTTP_POST | HTTP_PUT);
|
||||
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request /msgpack2 : "); // should print: Body request /msgpack2 : {"name":"Bob"}
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncMessagePackResponse *response = new AsyncMessagePackResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
@@ -77,6 +79,22 @@ void setup() {
|
||||
});
|
||||
|
||||
server.addHandler(handler);
|
||||
|
||||
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
|
||||
//
|
||||
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/msgpack3
|
||||
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack3
|
||||
//
|
||||
server.on("/msgpack3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request /msgpack3 : "); // should print: Body request /msgpack3 : {"name":"Bob"}
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to sue Middleware
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Query parameters and body parameters
|
||||
@@ -74,7 +74,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// - Download ESP32 partition by name and/or type and/or subtype
|
||||
@@ -34,7 +34,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Perf tests
|
||||
@@ -91,7 +91,7 @@ static volatile size_t requests = 0;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -118,9 +118,8 @@ void setup() {
|
||||
|
||||
// HTTP endpoint
|
||||
//
|
||||
// > brew install autocannon
|
||||
// > autocannon -c 10 -w 10 -d 20 http://192.168.4.1
|
||||
// > autocannon -c 16 -w 16 -d 20 http://192.168.4.1
|
||||
// > autocannon -c 16 -w 16 -d 20 --renderStatusCodes http://192.168.4.1/
|
||||
// > ab -c 16 -t 20 http://192.168.4.1/
|
||||
//
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
// need to cast to uint8_t*
|
||||
@@ -142,6 +141,11 @@ void setup() {
|
||||
//
|
||||
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
|
||||
//
|
||||
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
|
||||
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
|
||||
//
|
||||
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
|
||||
//
|
||||
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
requests = requests + 1;
|
||||
uint32_t d = request->getParam("d")->value().toInt();
|
||||
@@ -168,7 +172,6 @@ void setup() {
|
||||
// SSS endpoint
|
||||
//
|
||||
// launch 16 concurrent workers for 30 seconds
|
||||
// > for i in {1..10}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
|
||||
// > for i in {1..16}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
|
||||
//
|
||||
// With AsyncTCP, with 16 workers: a lot of "Event message queue overflow: discard message", no crash
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to rate limit the server or some endpoints
|
||||
@@ -25,7 +25,7 @@ static AsyncRateLimitMiddleware rateLimit;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to redirect
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
|
||||
@@ -34,7 +34,7 @@ static AsyncWebServerRequestPtr gpioRequest;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
|
||||
@@ -94,7 +94,7 @@ static bool processLongRunningOperation(LongRunningOperation *op) {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Make sure resumable downloads can be implemented (HEAD request / response and Range header)
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to rewrite URLs
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// SSE example
|
||||
@@ -58,7 +58,7 @@ static AsyncEventSource events("/events");
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -71,12 +71,12 @@ void setup() {
|
||||
});
|
||||
|
||||
events.onConnect([](AsyncEventSourceClient *client) {
|
||||
Serial.printf("SSE Client connected! ID: %" PRIu32 "\n", client->lastId());
|
||||
Serial.printf("SSE Client connected!");
|
||||
client->send("hello!", NULL, millis(), 1000);
|
||||
});
|
||||
|
||||
events.onDisconnect([](AsyncEventSourceClient *client) {
|
||||
Serial.printf("SSE Client disconnected! ID: %" PRIu32 "\n", client->lastId());
|
||||
Serial.printf("SSE Client disconnected!");
|
||||
});
|
||||
|
||||
server.addHandler(&events);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// SSE example
|
||||
@@ -64,7 +64,7 @@ static constexpr uint32_t timeoutClose = 15000;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Server state example
|
||||
@@ -25,7 +25,7 @@ static AsyncWebServer server2(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Authentication and authorization middlewares
|
||||
@@ -27,7 +27,7 @@ static AsyncLoggingMiddleware logging;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Simulate a slow response in a chunk response (like file download from SD Card)
|
||||
@@ -89,7 +89,7 @@ static size_t charactersIndex = 0;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -114,6 +114,11 @@ void setup() {
|
||||
//
|
||||
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
|
||||
//
|
||||
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
|
||||
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
|
||||
//
|
||||
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
|
||||
//
|
||||
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint32_t d = request->getParam("d")->value().toInt();
|
||||
uint32_t l = request->getParam("l")->value().toInt();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a static file
|
||||
@@ -111,7 +111,7 @@ static const size_t index2_html_gz_len = sizeof(index2_html_gz);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a static and dynamic template
|
||||
@@ -33,10 +33,23 @@ static const char *htmlContent PROGMEM = R"(
|
||||
|
||||
static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
|
||||
// Variables used for dynamic cacheable template
|
||||
static unsigned uptimeInMinutes = 0;
|
||||
static AsyncStaticWebHandler *uptimeHandler = nullptr;
|
||||
|
||||
// Utility function for performing that update
|
||||
static void setUptimeInMinutes(unsigned t) {
|
||||
uptimeInMinutes = t;
|
||||
// Update caching header with a new value as well
|
||||
if (uptimeHandler) {
|
||||
uptimeHandler->setLastModified();
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -56,35 +69,68 @@ void setup() {
|
||||
|
||||
// Serve the static template file
|
||||
//
|
||||
// This call will have caching headers automatically added as it is a static file.
|
||||
//
|
||||
// curl -v http://192.168.4.1/template.html
|
||||
server.serveStatic("/template.html", LittleFS, "/template.html");
|
||||
|
||||
// Serve the static template with a template processor
|
||||
// Serve a template with dynamic content
|
||||
//
|
||||
// ServeStatic static is used to serve static output which never changes over time.
|
||||
// This special endpoints automatically adds caching headers.
|
||||
// If a template processor is used, it must ensure that the outputted content will always be the same over time and never changes.
|
||||
// Otherwise, do not use serveStatic.
|
||||
// Example below: IP never changes.
|
||||
// serveStatic recognizes that template processing is in use, and will not automatically
|
||||
// add caching headers.
|
||||
//
|
||||
// curl -v http://192.168.4.1/index.html
|
||||
server.serveStatic("/index.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
// curl -v http://192.168.4.1/dynamic.html
|
||||
server.serveStatic("/dynamic.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return "Bob";
|
||||
return String("Bob ") + millis();
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
|
||||
// Serve a template with dynamic content
|
||||
// Serve a static template with a template processor
|
||||
//
|
||||
// to serve a template with dynamic content (output changes over time), use normal
|
||||
// Example below: content changes over tinme do not use serveStatic.
|
||||
// By explicitly calling setLastModified() on the handler object, we enable
|
||||
// sending the caching headers, even when a template is in use.
|
||||
// This pattern should never be used with template data that can change.
|
||||
// Example below: USER never changes.
|
||||
//
|
||||
// curl -v http://192.168.4.1/dynamic.html
|
||||
server.on("/dynamic.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/template.html", "text/html", false, [](const String &var) -> String {
|
||||
// curl -v http://192.168.4.1/index.html
|
||||
server.serveStatic("/index.html", LittleFS, "/template.html")
|
||||
.setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return String("Bob ") + millis();
|
||||
return "Bob";
|
||||
}
|
||||
return emptyString;
|
||||
})
|
||||
.setLastModified("Sun, 28 Sep 2025 01:02:03 GMT");
|
||||
|
||||
// Serve a template with dynamic content *and* caching
|
||||
//
|
||||
// The data used in this template is updated in loop(). loop() is then responsible
|
||||
// for calling setLastModified() on the handler object to notify any caches that
|
||||
// the data has changed.
|
||||
//
|
||||
// curl -v http://192.168.4.1/uptime.html
|
||||
uptimeHandler = &server.serveStatic("/uptime.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return String("Bob ") + uptimeInMinutes + " minutes";
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
|
||||
// Serve a template with dynamic content based on user request
|
||||
//
|
||||
// In this case, the template is served via a callback request. Data from the request
|
||||
// is used to generate the template callback.
|
||||
//
|
||||
// curl -v -G -d "USER=Bob" http://192.168.4.1/user_request.html
|
||||
server.on("/user_request.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/template.html", "text/html", false, [=](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
const AsyncWebParameter *param = request->getParam("USER");
|
||||
if (param) {
|
||||
return param->value();
|
||||
}
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
@@ -96,4 +142,11 @@ void setup() {
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
|
||||
// Compute uptime
|
||||
unsigned currentUptimeInMinutes = millis() / (60 * 1000);
|
||||
|
||||
if (currentUptimeInMinutes != uptimeInMinutes) {
|
||||
setUptimeInMinutes(currentUptimeInMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
349
examples/URIMatcher/README.md
Normal file
349
examples/URIMatcher/README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# AsyncURIMatcher Example
|
||||
|
||||
This example demonstrates the comprehensive URI matching capabilities of the ESPAsyncWebServer library using the `AsyncURIMatcher` class.
|
||||
|
||||
## Overview
|
||||
|
||||
The `AsyncURIMatcher` class provides flexible and powerful URL routing mechanisms that go beyond simple string matching. It supports various matching strategies that can be combined to create sophisticated routing rules.
|
||||
|
||||
**Important**: When using plain strings (not `AsyncURIMatcher` objects), the library uses auto-detection (`URIMatchAuto`) which analyzes the URI pattern and applies appropriate matching rules. This is **not** simple exact matching - it combines exact and folder matching by default!
|
||||
|
||||
## What's Demonstrated
|
||||
|
||||
This example includes two Arduino sketches:
|
||||
|
||||
1. **URIMatcher.ino** - Interactive web-based demonstration with a user-friendly homepage
|
||||
2. **URIMatcherTest.ino** - Comprehensive test suite with automated shell script testing
|
||||
|
||||
Both sketches create a WiFi Access Point (`esp-captive`) for easy testing without network configuration.
|
||||
|
||||
## Auto-Detection Behavior
|
||||
|
||||
When you pass a plain string or `const char*` to `server.on()`, the `URIMatchAuto` flag is used, which:
|
||||
|
||||
1. **Empty URI**: Matches everything
|
||||
2. **Ends with `*`**: Becomes prefix match (`URIMatchPrefix`)
|
||||
3. **Contains `/*.ext`**: Becomes extension match (`URIMatchExtension`)
|
||||
4. **Starts with `^` and ends with `$`**: Becomes regex match (if enabled)
|
||||
5. **Everything else**: Becomes **both** exact and folder match (`URIMatchPrefixFolder | URIMatchExact`)
|
||||
|
||||
This means traditional string-based routes like `server.on("/path", handler)` will match:
|
||||
|
||||
- `/path` (exact match)
|
||||
- `/path/` (folder with trailing slash)
|
||||
- `/path/anything` (folder match)
|
||||
|
||||
But will **NOT** match `/path-suffix` (prefix without folder separator).
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. **Auto-Detection (Traditional Behavior)**
|
||||
|
||||
Demonstrates how traditional string-based routing automatically combines exact and folder matching.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `/auto` - Matches both `/auto` exactly AND `/auto/sub` as folder
|
||||
- `/wildcard*` - Auto-detects as prefix match (due to trailing `*`)
|
||||
- `/auto-images/*.png` - Auto-detects as extension match (due to `/*.ext` pattern)
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `/exact` - Matches `/exact`, `/exact/`, and `/exact/sub`
|
||||
- `/api/users` - Matches exact path and subpaths under `/api/users/`
|
||||
- `/*.json` - Matches any `.json` file anywhere
|
||||
- `/*.css` - Matches any `.css` file anywhere
|
||||
|
||||
### 2. **Exact Matching (Factory Method)**
|
||||
|
||||
Using `AsyncURIMatcher::exact()` matches only the exact URL, **NOT** subpaths.
|
||||
|
||||
**Key difference from auto-detection:** `AsyncURIMatcher::exact("/path")` matches **only** `/path`, while `server.on("/path", ...)` matches both `/path` and `/path/sub`.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/exact")` - Matches only `/exact`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/factory/exact")` - Matches only `/factory/exact`
|
||||
- Does NOT match `/factory/exact/sub` (404 response)
|
||||
|
||||
### 3. **Prefix Matching**
|
||||
|
||||
Using `AsyncURIMatcher::prefix()` matches URLs that start with the specified pattern.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/service")` - Matches `/service`, `/service-test`, `/service/status`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/factory/prefix")` - Matches `/factory/prefix`, `/factory/prefix-test`, `/factory/prefix/sub`
|
||||
- Traditional: `/api/*` - Matches `/api/data`, `/api/v1/posts`
|
||||
- Traditional: `/files/*` - Matches `/files/document.pdf`, `/files/images/photo.jpg`
|
||||
|
||||
### 4. **Folder/Directory Matching**
|
||||
|
||||
Using `AsyncURIMatcher::dir()` matches URLs under a directory (automatically adds trailing slash).
|
||||
|
||||
**Important:** Directory matching requires a trailing slash in the URL - it does NOT match the directory itself.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::dir("/admin")` - Matches `/admin/users`, `/admin/settings`
|
||||
- Does NOT match `/admin` without trailing slash
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::dir("/factory/dir")` - Matches `/factory/dir/users`, `/factory/dir/sub/path`
|
||||
- Does NOT match `/factory/dir` itself (404 response)
|
||||
|
||||
### 5. **Extension Matching**
|
||||
|
||||
Using `AsyncURIMatcher::ext()` matches files with specific extensions.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::ext("/images/*.jpg")` - Matches `/images/photo.jpg`, `/images/sub/pic.jpg`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::ext("/factory/files/*.txt")` - Matches `/factory/files/doc.txt`, `/factory/files/sub/readme.txt`
|
||||
- Does NOT match `/factory/files/doc.pdf` (wrong extension)
|
||||
|
||||
### 6. **Case Insensitive Matching**
|
||||
|
||||
Using `AsyncURIMatcher::CaseInsensitive` flag matches URLs regardless of character case.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive)` - Matches `/case`, `/CASE`, `/CaSe`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- Case insensitive exact: `/case/exact`, `/CASE/EXACT`, `/Case/Exact` all work
|
||||
- Case insensitive prefix: `/case/prefix`, `/CASE/PREFIX-test`, `/Case/Prefix/sub` all work
|
||||
- Case insensitive directory: `/case/dir/users`, `/CASE/DIR/admin`, `/Case/Dir/settings` all work
|
||||
- Case insensitive extension: `/case/files/doc.pdf`, `/CASE/FILES/DOC.PDF`, `/Case/Files/Doc.Pdf` all work
|
||||
|
||||
### 7. **Regular Expression Matching**
|
||||
|
||||
Using `AsyncURIMatcher::regex()` for advanced pattern matching (requires `ASYNCWEBSERVER_REGEX`).
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
```cpp
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
AsyncURIMatcher::regex("^/user/([0-9]+)$") // Matches /user/123, captures ID
|
||||
#endif
|
||||
```
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- Traditional regex: `^/user/([0-9]+)$` - Matches `/user/123`, `/user/456`
|
||||
- Traditional regex: `^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$` - Matches `/blog/2023/10/15`
|
||||
- Factory regex: `AsyncURIMatcher::regex("^/factory/user/([0-9]+)$")` - Matches `/factory/user/123`
|
||||
- Factory regex with multiple captures: `^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$`
|
||||
- Case insensitive regex: `AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive)`
|
||||
|
||||
### 8. **Combined Flags**
|
||||
|
||||
Multiple matching strategies can be combined using the `|` operator.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive)` - Prefix match that's case insensitive
|
||||
|
||||
### 9. **Special Matchers**
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::all()` - Matches all requests (used with POST method as catch-all)
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Traditional String-based Routing (Auto-Detection)
|
||||
|
||||
```cpp
|
||||
// Auto-detection with exact + folder matching
|
||||
server.on("/api", handler); // Matches /api AND /api/anything
|
||||
server.on("/login", handler); // Matches /login AND /login/sub
|
||||
|
||||
// Auto-detection with prefix matching
|
||||
server.on("/prefix*", handler); // Matches /prefix, /prefix-test, /prefix/sub
|
||||
|
||||
// Auto-detection with extension matching
|
||||
server.on("/images/*.jpg", handler); // Matches /images/pic.jpg, /images/sub/pic.jpg
|
||||
```
|
||||
|
||||
### Explicit AsyncURIMatcher Syntax
|
||||
|
||||
### Explicit AsyncURIMatcher Syntax
|
||||
|
||||
```cpp
|
||||
// Exact matching only
|
||||
server.on(AsyncURIMatcher("/path", URIMatchExact), handler);
|
||||
|
||||
// Prefix matching only
|
||||
server.on(AsyncURIMatcher("/api", URIMatchPrefix), handler);
|
||||
|
||||
// Combined flags
|
||||
server.on(AsyncURIMatcher("/api", URIMatchPrefix | URIMatchCaseInsensitive), handler);
|
||||
```
|
||||
|
||||
### Factory Functions
|
||||
|
||||
```cpp
|
||||
// More readable and expressive
|
||||
server.on(AsyncURIMatcher::exact("/login"), handler);
|
||||
server.on(AsyncURIMatcher::prefix("/api"), handler);
|
||||
server.on(AsyncURIMatcher::dir("/admin"), handler);
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler);
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
|
||||
#endif
|
||||
```
|
||||
|
||||
## Available Flags
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------- | ----------------------------------------------------------- |
|
||||
| `URIMatchAuto` | Auto-detect match type from pattern (default) |
|
||||
| `URIMatchExact` | Exact URL match |
|
||||
| `URIMatchPrefix` | Prefix match |
|
||||
| `URIMatchPrefixFolder` | Folder prefix match (requires trailing /) |
|
||||
| `URIMatchExtension` | File extension match pattern |
|
||||
| `URIMatchCaseInsensitive` | Case insensitive matching |
|
||||
| `URIMatchRegex` | Regular expression matching (requires ASYNCWEBSERVER_REGEX) |
|
||||
|
||||
## Testing the Example
|
||||
|
||||
1. **Upload the sketch** to your ESP32/ESP8266
|
||||
2. **Connect to WiFi AP**: `esp-captive` (no password required)
|
||||
3. **Navigate to**: `http://192.168.4.1/`
|
||||
4. **Explore the examples** by clicking the organized test links
|
||||
5. **Monitor Serial output**: Open Serial Monitor to see detailed debugging information for each matched route
|
||||
|
||||
### Test URLs Available (All Clickable from Homepage)
|
||||
|
||||
**Auto-Detection Examples:**
|
||||
|
||||
- `http://192.168.4.1/auto` (exact + folder match)
|
||||
- `http://192.168.4.1/auto/sub` (folder match - same handler!)
|
||||
- `http://192.168.4.1/wildcard-test` (auto-detected prefix)
|
||||
- `http://192.168.4.1/auto-images/photo.png` (auto-detected extension)
|
||||
|
||||
**Factory Method Examples:**
|
||||
|
||||
- `http://192.168.4.1/exact` (AsyncURIMatcher::exact)
|
||||
- `http://192.168.4.1/service/status` (AsyncURIMatcher::prefix)
|
||||
- `http://192.168.4.1/admin/users` (AsyncURIMatcher::dir)
|
||||
- `http://192.168.4.1/images/photo.jpg` (AsyncURIMatcher::ext)
|
||||
|
||||
**Case Insensitive Examples:**
|
||||
|
||||
- `http://192.168.4.1/case` (lowercase)
|
||||
- `http://192.168.4.1/CASE` (uppercase)
|
||||
- `http://192.168.4.1/CaSe` (mixed case)
|
||||
|
||||
**Regex Examples (if ASYNCWEBSERVER_REGEX enabled):**
|
||||
|
||||
- `http://192.168.4.1/user/123` (captures numeric ID)
|
||||
- `http://192.168.4.1/user/456` (captures numeric ID)
|
||||
|
||||
**Combined Flags Examples:**
|
||||
|
||||
- `http://192.168.4.1/mixedcase-test` (prefix + case insensitive)
|
||||
- `http://192.168.4.1/MIXEDCASE/sub` (prefix + case insensitive)
|
||||
|
||||
### Console Output
|
||||
|
||||
Each handler provides detailed debugging information via Serial output:
|
||||
|
||||
```
|
||||
Auto-Detection Match (Traditional)
|
||||
Matched URL: /auto
|
||||
Uses auto-detection: exact + folder matching
|
||||
```
|
||||
|
||||
```
|
||||
Factory Exact Match
|
||||
Matched URL: /exact
|
||||
Uses AsyncURIMatcher::exact() factory function
|
||||
```
|
||||
|
||||
```
|
||||
Regex Match - User ID
|
||||
Matched URL: /user/123
|
||||
Captured User ID: 123
|
||||
This regex matches /user/{number} pattern
|
||||
```
|
||||
|
||||
## Compilation Options
|
||||
|
||||
### Enable Regex Support
|
||||
|
||||
To enable regular expression matching, compile with:
|
||||
|
||||
```
|
||||
-D ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
In PlatformIO, add to `platformio.ini`:
|
||||
|
||||
```ini
|
||||
build_flags = -D ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
In Arduino IDE, add to your sketch:
|
||||
|
||||
```cpp
|
||||
#define ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Exact matches** are fastest
|
||||
2. **Prefix matches** are very efficient
|
||||
3. **Regex matches** are slower but most flexible
|
||||
4. **Case insensitive** matching adds minimal overhead
|
||||
5. **Auto-detection** adds slight parsing overhead at construction time
|
||||
|
||||
## Real-World Applications
|
||||
|
||||
### REST API Design
|
||||
|
||||
```cpp
|
||||
// API versioning
|
||||
server.on(AsyncURIMatcher::prefix("/api/v1"), handleAPIv1);
|
||||
server.on(AsyncURIMatcher::prefix("/api/v2"), handleAPIv2);
|
||||
|
||||
// Resource endpoints with IDs
|
||||
server.on(AsyncURIMatcher::regex("^/api/users/([0-9]+)$"), handleUserById);
|
||||
server.on(AsyncURIMatcher::regex("^/api/posts/([0-9]+)/comments$"), handlePostComments);
|
||||
```
|
||||
|
||||
### File Serving
|
||||
|
||||
```cpp
|
||||
// Serve different file types
|
||||
server.on(AsyncURIMatcher::ext("/assets/*.css"), serveCSSFiles);
|
||||
server.on(AsyncURIMatcher::ext("/assets/*.js"), serveJSFiles);
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), serveImageFiles);
|
||||
```
|
||||
|
||||
### Admin Interface
|
||||
|
||||
```cpp
|
||||
// Admin section with authentication
|
||||
server.on(AsyncURIMatcher::dir("/admin"), handleAdminPages);
|
||||
server.on(AsyncURIMatcher::exact("/admin"), redirectToAdminDashboard);
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [ESPAsyncWebServer Documentation](https://github.com/ESP32Async/ESPAsyncWebServer)
|
||||
- [Regular Expression Reference](https://en.cppreference.com/w/cpp/regex)
|
||||
- Other examples in the `examples/` directory
|
||||
276
examples/URIMatcher/URIMatcher.ino
Normal file
276
examples/URIMatcher/URIMatcher.ino
Normal file
@@ -0,0 +1,276 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// AsyncURIMatcher Examples - Advanced URI Matching and Routing
|
||||
//
|
||||
// This example demonstrates the various ways to use AsyncURIMatcher class
|
||||
// for flexible URL routing with different matching strategies:
|
||||
//
|
||||
// 1. Exact matching
|
||||
// 2. Prefix matching
|
||||
// 3. Folder/directory matching
|
||||
// 4. Extension matching
|
||||
// 5. Case insensitive matching
|
||||
// 6. Regex matching (if ASYNCWEBSERVER_REGEX is enabled)
|
||||
// 7. Factory functions for common patterns
|
||||
//
|
||||
// Test URLs:
|
||||
// - Exact: http://192.168.4.1/exact
|
||||
// - Prefix: http://192.168.4.1/prefix-anything
|
||||
// - Folder: http://192.168.4.1/api/users, http://192.168.4.1/api/posts
|
||||
// - Extension: http://192.168.4.1/images/photo.jpg, http://192.168.4.1/docs/readme.pdf
|
||||
// - Case insensitive: http://192.168.4.1/CaSe or http://192.168.4.1/case
|
||||
// - Wildcard: http://192.168.4.1/wildcard-test
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <RPAsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println();
|
||||
Serial.println("=== AsyncURIMatcher Example ===");
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// 1. AUTO-DETECTION BEHAVIOR - traditional string-based routing
|
||||
// =============================================================================
|
||||
|
||||
// Traditional string-based routing with auto-detection
|
||||
// This uses URIMatchAuto which combines URIMatchPrefixFolder | URIMatchExact
|
||||
// It will match BOTH "/auto" exactly AND "/auto/" + anything
|
||||
server.on("/auto", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detection Match (Traditional)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses auto-detection: exact + folder matching");
|
||||
request->send(200, "text/plain", "OK - Auto-detection match");
|
||||
});
|
||||
|
||||
// Auto-detection for wildcard patterns (ends with *)
|
||||
// This auto-detects as URIMatchPrefix
|
||||
server.on("/wildcard*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detected Wildcard (Prefix)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Auto-detected as prefix match due to trailing *");
|
||||
request->send(200, "text/plain", "OK - Wildcard prefix match");
|
||||
});
|
||||
|
||||
// Auto-detection for extension patterns (contains /*.ext)
|
||||
// This auto-detects as URIMatchExtension
|
||||
server.on("/auto-images/*.png", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detected Extension Pattern");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Auto-detected as extension match due to /*.png pattern");
|
||||
request->send(200, "text/plain", "OK - Extension match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 2. EXACT MATCHING - matches only the exact URL (explicit)
|
||||
// =============================================================================
|
||||
|
||||
// Using factory function for exact match
|
||||
server.on(AsyncURIMatcher::exact("/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Factory Exact Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses AsyncURIMatcher::exact() factory function");
|
||||
request->send(200, "text/plain", "OK - Factory exact match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 3. PREFIX MATCHING - matches URLs that start with the pattern
|
||||
// =============================================================================
|
||||
|
||||
// Using factory function for prefix match
|
||||
server.on(AsyncURIMatcher::prefix("/service"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Service Prefix Match (Factory)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses AsyncURIMatcher::prefix() factory function");
|
||||
request->send(200, "text/plain", "OK - Factory prefix match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 4. FOLDER/DIRECTORY MATCHING - matches URLs in a folder structure
|
||||
// =============================================================================
|
||||
|
||||
// Folder match using factory function (automatically adds trailing slash)
|
||||
server.on(AsyncURIMatcher::dir("/admin"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Admin Directory Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches URLs under /admin/ directory");
|
||||
Serial.println("Note: /admin (without slash) will NOT match");
|
||||
request->send(200, "text/plain", "OK - Directory match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 5. EXTENSION MATCHING - matches files with specific extensions
|
||||
// =============================================================================
|
||||
|
||||
// Image extension matching
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("JPG Image Handler");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches any .jpg file under /images/");
|
||||
request->send(200, "text/plain", "OK - Extension match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 6. CASE INSENSITIVE MATCHING
|
||||
// =============================================================================
|
||||
|
||||
// Case insensitive exact match
|
||||
server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Case Insensitive Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches /case in any case combination");
|
||||
request->send(200, "text/plain", "OK - Case insensitive match");
|
||||
});
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
// =============================================================================
|
||||
// 7. REGEX MATCHING (only available if ASYNCWEBSERVER_REGEX is enabled)
|
||||
// =============================================================================
|
||||
|
||||
// Regex match for numeric IDs
|
||||
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Regex Match - User ID");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
if (request->pathArg(0).length() > 0) {
|
||||
Serial.println("Captured User ID: " + request->pathArg(0));
|
||||
}
|
||||
Serial.println("This regex matches /user/{number} pattern");
|
||||
request->send(200, "text/plain", "OK - Regex match");
|
||||
});
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// 8. COMBINED FLAGS EXAMPLE
|
||||
// =============================================================================
|
||||
|
||||
// Combine multiple flags
|
||||
server.on(AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Combined Flags Example");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses both AsyncURIMatcher::Prefix and AsyncURIMatcher::CaseInsensitive");
|
||||
request->send(200, "text/plain", "OK - Combined flags match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 9. HOMEPAGE WITH NAVIGATION
|
||||
// =============================================================================
|
||||
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Homepage accessed");
|
||||
String response = R"(<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AsyncURIMatcher Examples</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #2E86AB; }
|
||||
.test-link { display: block; margin: 5px 0; padding: 8px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }
|
||||
.test-link:hover { background: #e0e0e0; }
|
||||
.section { margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AsyncURIMatcher Examples</h1>
|
||||
|
||||
<div class="section">
|
||||
<h3>Auto-Detection (Traditional String Matching)</h3>
|
||||
<a href="/auto" class="test-link">/auto (auto-detection: exact + folder)</a>
|
||||
<a href="/auto/sub" class="test-link">/auto/sub (folder match)</a>
|
||||
<a href="/wildcard-test" class="test-link">/wildcard-test (auto prefix)</a>
|
||||
<a href="/auto-images/photo.png" class="test-link">/auto-images/photo.png (auto extension)</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Factory Method Examples</h3>
|
||||
<a href="/exact" class="test-link">/exact (factory exact)</a>
|
||||
<a href="/service/status" class="test-link">/service/status (factory prefix)</a>
|
||||
<a href="/admin/users" class="test-link">/admin/users (factory directory)</a>
|
||||
<a href="/images/photo.jpg" class="test-link">/images/photo.jpg (factory extension)</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Case Insensitive Matching</h3>
|
||||
<a href="/case" class="test-link">/case (lowercase)</a>
|
||||
<a href="/CASE" class="test-link">/CASE (uppercase)</a>
|
||||
<a href="/CaSe" class="test-link">/CaSe (mixed case)</a>
|
||||
</div>
|
||||
|
||||
)";
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
response += R"( <div class="section">
|
||||
<h3>Regex Matching</h3>
|
||||
<a href="/user/123" class="test-link">/user/123 (regex numeric ID)</a>
|
||||
<a href="/user/456" class="test-link">/user/456 (regex numeric ID)</a>
|
||||
</div>
|
||||
|
||||
)";
|
||||
#endif
|
||||
response += R"( <div class="section">
|
||||
<h3>Combined Flags</h3>
|
||||
<a href="/mixedcase-test" class="test-link">/mixedcase-test (prefix + case insensitive)</a>
|
||||
<a href="/MIXEDCASE/sub" class="test-link">/MIXEDCASE/sub (prefix + case insensitive)</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>)";
|
||||
request->send(200, "text/html", response);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 10. NOT FOUND HANDLER
|
||||
// =============================================================================
|
||||
|
||||
server.onNotFound([](AsyncWebServerRequest *request) {
|
||||
String html = "<h1>404 - Not Found</h1>";
|
||||
html += "<p>The requested URL <strong>" + request->url() + "</strong> was not found.</p>";
|
||||
html += "<p><a href='/'>← Back to Examples</a></p>";
|
||||
request->send(404, "text/html", html);
|
||||
});
|
||||
|
||||
server.begin();
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== Server Started ===");
|
||||
Serial.println("Open your browser and navigate to:");
|
||||
Serial.println("http://192.168.4.1/ - Main examples page");
|
||||
Serial.println();
|
||||
Serial.println("Available test endpoints:");
|
||||
Serial.println("• Auto-detection: /auto (exact+folder), /wildcard*, /auto-images/*.png");
|
||||
Serial.println("• Exact matches: /exact");
|
||||
Serial.println("• Prefix matches: /service*");
|
||||
Serial.println("• Folder matches: /admin/*");
|
||||
Serial.println("• Extension matches: /images/*.jpg");
|
||||
Serial.println("• Case insensitive: /case (try /CASE, /Case)");
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
Serial.println("• Regex matches: /user/123");
|
||||
#endif
|
||||
Serial.println("• Combined flags: /mixedcase*");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Nothing to do here - the server handles everything asynchronously
|
||||
delay(1000);
|
||||
}
|
||||
165
examples/URIMatcherTest/URIMatcherTest.ino
Normal file
165
examples/URIMatcherTest/URIMatcherTest.ino
Normal file
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Test for ESPAsyncWebServer URI matching
|
||||
//
|
||||
// Usage: upload, connect to the AP and run test_routes.sh
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <RPAsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// Status endpoint
|
||||
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Exact paths, plus the subpath (/exact matches /exact/sub but not /exact-no-match)
|
||||
server.on("/exact", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Prefix matching
|
||||
server.on("/api/*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("/files/*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Extensions
|
||||
server.on("/*.json", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "application/json", "{\"status\":\"OK\"}");
|
||||
});
|
||||
|
||||
server.on("/*.css", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/css", "/* OK */");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// NEW ASYNCURIMATCHER FACTORY METHODS TESTS
|
||||
// =============================================================================
|
||||
|
||||
// Exact match using factory method (does NOT match subpaths like traditional)
|
||||
server.on(AsyncURIMatcher::exact("/factory/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Prefix match using factory method
|
||||
server.on(AsyncURIMatcher::prefix("/factory/prefix"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Directory match using factory method (matches /dir/anything but not /dir itself)
|
||||
server.on(AsyncURIMatcher::dir("/factory/dir"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Extension match using factory method
|
||||
server.on(AsyncURIMatcher::ext("/factory/files/*.txt"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// CASE INSENSITIVE MATCHING TESTS
|
||||
// =============================================================================
|
||||
|
||||
// Case insensitive exact match
|
||||
server.on(AsyncURIMatcher::exact("/case/exact", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive prefix match
|
||||
server.on(AsyncURIMatcher::prefix("/case/prefix", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive directory match
|
||||
server.on(AsyncURIMatcher::dir("/case/dir", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive extension match
|
||||
server.on(AsyncURIMatcher::ext("/case/files/*.PDF", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
// Traditional regex patterns (backward compatibility)
|
||||
server.on("^/user/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// NEW ASYNCURIMATCHER REGEX FACTORY METHODS
|
||||
// =============================================================================
|
||||
|
||||
// Regex match using factory method
|
||||
server.on(AsyncURIMatcher::regex("^/factory/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive regex match using factory method
|
||||
server.on(AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Complex regex with multiple capture groups
|
||||
server.on(AsyncURIMatcher::regex("^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// SPECIAL MATCHERS
|
||||
// =============================================================================
|
||||
|
||||
// Match all POST requests (catch-all before 404)
|
||||
server.on(AsyncURIMatcher::all(), HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
server.onNotFound([](AsyncWebServerRequest *request) {
|
||||
request->send(404, "text/plain", "Not Found");
|
||||
});
|
||||
|
||||
server.begin();
|
||||
Serial.println("Server ready");
|
||||
}
|
||||
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
174
examples/URIMatcherTest/test_routes.sh
Executable file
174
examples/URIMatcherTest/test_routes.sh
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
|
||||
# URI Matcher Test Script
|
||||
# Tests all routes defined in URIMatcherTest.ino
|
||||
|
||||
SERVER_IP="${1:-192.168.4.1}"
|
||||
SERVER_PORT="80"
|
||||
BASE_URL="http://${SERVER_IP}:${SERVER_PORT}"
|
||||
|
||||
echo "Testing URI Matcher at $BASE_URL"
|
||||
echo "=================================="
|
||||
|
||||
# Function to test a route
|
||||
test_route() {
|
||||
local path="$1"
|
||||
local expected_status="$2"
|
||||
local description="$3"
|
||||
|
||||
echo -n "Testing $path ... "
|
||||
|
||||
response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL$path" 2>/dev/null)
|
||||
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
|
||||
|
||||
if [ "$status_code" = "$expected_status" ]; then
|
||||
echo "✅ PASS ($status_code)"
|
||||
else
|
||||
echo "❌ FAIL (expected $expected_status, got $status_code)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test counter
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# Test all routes that should return 200 OK
|
||||
echo "Testing routes that should work (200 OK):"
|
||||
|
||||
if test_route "/status" "200" "Status endpoint"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact" "200" "Exact path"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact/" "200" "Exact path ending with /"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact/sub" "200" "Exact path with subpath /"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/users" "200" "Exact API path"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/data" "200" "API prefix match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/v1/posts" "200" "API prefix deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/files/document.pdf" "200" "Files prefix"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/files/images/photo.jpg" "200" "Files prefix deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/config.json" "200" "JSON extension"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/data/settings.json" "200" "JSON extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/style.css" "200" "CSS extension"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/assets/main.css" "200" "CSS extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing AsyncURIMatcher factory methods:"
|
||||
|
||||
# Factory exact match (should NOT match subpaths)
|
||||
if test_route "/factory/exact" "200" "Factory exact match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory prefix match
|
||||
if test_route "/factory/prefix" "200" "Factory prefix base"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/prefix-test" "200" "Factory prefix extended"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/prefix/sub" "200" "Factory prefix subpath"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory directory match (should NOT match the directory itself)
|
||||
if test_route "/factory/dir/users" "200" "Factory directory match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/dir/sub/path" "200" "Factory directory deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory extension match
|
||||
if test_route "/factory/files/doc.txt" "200" "Factory extension match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/files/sub/readme.txt" "200" "Factory extension deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing case insensitive matching:"
|
||||
|
||||
# Case insensitive exact
|
||||
if test_route "/case/exact" "200" "Case exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/EXACT" "200" "Case exact uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Exact" "200" "Case exact mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive prefix
|
||||
if test_route "/case/prefix" "200" "Case prefix lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/PREFIX-test" "200" "Case prefix uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Prefix/sub" "200" "Case prefix mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive directory
|
||||
if test_route "/case/dir/users" "200" "Case dir lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/DIR/admin" "200" "Case dir uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Dir/settings" "200" "Case dir mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive extension
|
||||
if test_route "/case/files/doc.pdf" "200" "Case ext lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/FILES/DOC.PDF" "200" "Case ext uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Files/Doc.Pdf" "200" "Case ext mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing special matchers:"
|
||||
|
||||
# Test POST to catch-all (all() matcher)
|
||||
echo -n "Testing POST /any/path (all matcher) ... "
|
||||
response=$(curl -s -X POST -w "HTTPSTATUS:%{http_code}" "$BASE_URL/any/path" 2>/dev/null)
|
||||
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
|
||||
if [ "$status_code" = "200" ]; then
|
||||
echo "✅ PASS ($status_code)"
|
||||
((PASS++))
|
||||
else
|
||||
echo "❌ FAIL (expected 200, got $status_code)"
|
||||
((FAIL++))
|
||||
fi
|
||||
|
||||
# Check if regex is enabled by testing the server
|
||||
echo ""
|
||||
echo "Checking for regex support..."
|
||||
regex_test=$(curl -s "$BASE_URL/user/123" 2>/dev/null)
|
||||
if curl -s -w "%{http_code}" "$BASE_URL/user/123" 2>/dev/null | grep -q "200"; then
|
||||
echo "Regex support detected - testing traditional regex routes:"
|
||||
if test_route "/user/123" "200" "Traditional regex user ID"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/user/456" "200" "Traditional regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/2023/10/15" "200" "Traditional regex blog date"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/2024/12/25" "200" "Traditional regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo "Testing AsyncURIMatcher regex factory methods:"
|
||||
if test_route "/factory/user/123" "200" "Factory regex user ID"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/user/789" "200" "Factory regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/blog/2023/10/15" "200" "Factory regex blog date"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/blog/2024/12/31" "200" "Factory regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive regex
|
||||
if test_route "/factory/search/hello" "200" "Factory regex search lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/FACTORY/SEARCH/WORLD" "200" "Factory regex search uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Factory/Search/Test" "200" "Factory regex search mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test regex validation
|
||||
if test_route "/user/abc" "404" "Invalid regex (letters instead of numbers)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/23/10/15" "404" "Invalid regex (2-digit year)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/user/abc" "404" "Factory regex invalid (letters)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
else
|
||||
echo "Regex support not detected (compile with ASYNCWEBSERVER_REGEX to enable)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Testing routes that should fail (404 Not Found):"
|
||||
|
||||
if test_route "/nonexistent" "404" "Non-existent route"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test factory exact vs traditional behavior difference
|
||||
if test_route "/factory/exact/sub" "404" "Factory exact should NOT match subpaths"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test factory directory requires trailing slash
|
||||
if test_route "/factory/dir" "404" "Factory directory should NOT match without trailing slash"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test extension mismatch
|
||||
if test_route "/factory/files/doc.pdf" "404" "Factory extension mismatch (.pdf vs .txt)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test case sensitive when flag not used
|
||||
if test_route "/exact" "200" "Traditional exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/EXACT" "404" "Traditional exact should be case sensitive"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Test Results:"
|
||||
echo "✅ Passed: $PASS"
|
||||
echo "❌ Failed: $FAIL"
|
||||
echo "Total: $((PASS + FAIL))"
|
||||
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo ""
|
||||
echo "🎉 All tests passed! URI matching is working correctly."
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Some tests failed. Check the server and routes."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Demo text, binary and file upload
|
||||
@@ -31,7 +31,7 @@ void setup() {
|
||||
LittleFS.begin();
|
||||
}
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -63,6 +63,7 @@ void setup() {
|
||||
if (!buffer->reserve(size)) {
|
||||
delete buffer;
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
request->_tempObject = buffer;
|
||||
}
|
||||
@@ -100,6 +101,7 @@ void setup() {
|
||||
|
||||
if (!request->_tempFile) {
|
||||
request->send(400, "text/plain", "File not available for writing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (len) {
|
||||
@@ -141,6 +143,7 @@ void setup() {
|
||||
|
||||
// first pass ?
|
||||
if (!index) {
|
||||
// Note: using content type to determine size is not reliable!
|
||||
size_t size = request->header("Content-Length").toInt();
|
||||
if (!size) {
|
||||
request->send(400, "text/plain", "No Content-Length");
|
||||
@@ -150,6 +153,7 @@ void setup() {
|
||||
if (!buffer) {
|
||||
// not enough memory
|
||||
request->abort();
|
||||
return;
|
||||
} else {
|
||||
request->_tempObject = buffer;
|
||||
}
|
||||
|
||||
158
examples/UploadFlash/UploadFlash.ino
Normal file
158
examples/UploadFlash/UploadFlash.ino
Normal file
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Demo to upload a firmware and filesystem image via multipart form data
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <RPAsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <StreamString.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
// ESP32 example ONLY
|
||||
#ifdef ESP32
|
||||
#include <Update.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
if (!LittleFS.begin()) {
|
||||
LittleFS.format();
|
||||
LittleFS.begin();
|
||||
}
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// ESP32 example ONLY
|
||||
#ifdef ESP32
|
||||
|
||||
// Shows how to get the fw and fs (names) and filenames from a multipart upload,
|
||||
// and also how to handle multiple file uploads in a single request.
|
||||
//
|
||||
// This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition.
|
||||
//
|
||||
// Execute in the terminal, in order:
|
||||
//
|
||||
// 1. Build firmware: pio run -e arduino-3
|
||||
// 2. Build FS image: pio run -e arduino-3 -t buildfs
|
||||
// 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill
|
||||
//
|
||||
server.on(
|
||||
"/flash", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if (request->getResponse()) {
|
||||
// response already created
|
||||
return;
|
||||
}
|
||||
|
||||
// list all parameters
|
||||
Serial.println("Request parameters:");
|
||||
const size_t params = request->params();
|
||||
for (size_t i = 0; i < params; i++) {
|
||||
const AsyncWebParameter *p = request->getParam(i);
|
||||
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
|
||||
}
|
||||
|
||||
Serial.println("Flash / Filesystem upload completed");
|
||||
|
||||
request->send(200, "text/plain", "Upload complete");
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);
|
||||
|
||||
if (request->getResponse() != nullptr) {
|
||||
// upload aborted
|
||||
return;
|
||||
}
|
||||
|
||||
// start a new content-disposition upload
|
||||
if (!index) {
|
||||
// list all parameters
|
||||
const size_t params = request->params();
|
||||
for (size_t i = 0; i < params; i++) {
|
||||
const AsyncWebParameter *p = request->getParam(i);
|
||||
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
|
||||
}
|
||||
|
||||
// get the content-disposition parameter
|
||||
const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true);
|
||||
if (p == nullptr) {
|
||||
request->send(400, "text/plain", "Missing content-disposition 'name' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// determine upload type based on the parameter name
|
||||
if (p->value() == "fs") {
|
||||
Serial.printf("Filesystem image upload for file: %s\n", filename.c_str());
|
||||
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update begin failed");
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (p->value() == "fw") {
|
||||
Serial.printf("Firmware image upload for file: %s\n", filename.c_str());
|
||||
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update begin failed");
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
Serial.printf("Unknown upload type for file: %s\n", filename.c_str());
|
||||
request->send(400, "text/plain", "Unknown upload type");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// some bytes to write ?
|
||||
if (len) {
|
||||
if (Update.write(data, len) != len) {
|
||||
Update.printError(Serial);
|
||||
Update.end();
|
||||
request->send(400, "text/plain", "Update write failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// finish the content-disposition upload
|
||||
if (final) {
|
||||
if (!Update.end(true)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update end failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// success response is created in the final request handler when all uploads are completed
|
||||
Serial.printf("Upload success of file %s\n", filename.c_str());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// WebSocket example
|
||||
@@ -19,17 +19,58 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
static const char *htmlContent PROGMEM = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Example</h1>
|
||||
<p>Open your browser console!</p>
|
||||
<input type="text" id="message" placeholder="Type a message">
|
||||
<button onclick='sendMessage()'>Send</button>
|
||||
<script>
|
||||
var ws = new WebSocket('ws://192.168.4.1/ws');
|
||||
ws.onopen = function() {
|
||||
console.log("WebSocket connected");
|
||||
};
|
||||
ws.onmessage = function(event) {
|
||||
console.log("WebSocket message: " + event.data);
|
||||
};
|
||||
ws.onclose = function() {
|
||||
console.log("WebSocket closed");
|
||||
};
|
||||
ws.onerror = function(error) {
|
||||
console.log("WebSocket error: " + error);
|
||||
};
|
||||
function sendMessage() {
|
||||
var message = document.getElementById("message").value;
|
||||
ws.send(message);
|
||||
console.log("WebSocket sent: " + message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
static AsyncWebSocket ws("/ws");
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// serves root html page
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength);
|
||||
});
|
||||
|
||||
//
|
||||
// Run in terminal 1: websocat ws://192.168.4.1/ws => should stream data
|
||||
// Run in terminal 2: websocat ws://192.168.4.1/ws => should stream data
|
||||
@@ -66,6 +107,7 @@ void setup() {
|
||||
if (info->opcode == WS_TEXT) {
|
||||
data[len] = 0;
|
||||
Serial.printf("ws text: %s\n", (char *)data);
|
||||
client->ping();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// WebSocket example using the easy to use AsyncWebSocketMessageHandler handler that only supports unfragmented messages
|
||||
@@ -40,7 +40,7 @@ static const char *htmlContent PROGMEM = R"(
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Example</h1>
|
||||
<>Open your browser console!</p>
|
||||
<p>Open your browser console!</p>
|
||||
<input type="text" id="message" placeholder="Type a message">
|
||||
<button onclick='sendMessage()'>Send</button>
|
||||
<script>
|
||||
@@ -71,7 +71,7 @@ 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
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -97,6 +97,7 @@ void setup() {
|
||||
|
||||
wsHandler.onMessage([](AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len) {
|
||||
Serial.printf("Client %" PRIu32 " data: %s\n", client->id(), (const char *)data);
|
||||
server->textAll(data, len);
|
||||
});
|
||||
|
||||
wsHandler.onFragment([](AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len) {
|
||||
|
||||
@@ -29,7 +29,7 @@ dependencies:
|
||||
version: "^3.1.1"
|
||||
require: public
|
||||
esp32async/asynctcp:
|
||||
version: "^3.4.7"
|
||||
version: "^3.4.10"
|
||||
require: public
|
||||
bblanchon/arduinojson:
|
||||
version: "^7.4.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to catch all requests and send a 404 Not Found response
|
||||
@@ -78,7 +78,7 @@ 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
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// SSE example
|
||||
@@ -50,7 +50,7 @@ static AsyncEventSource events("/events");
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// WebSocket example
|
||||
@@ -17,7 +17,7 @@ static AsyncWebSocket ws("/ws");
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ESPAsyncWebServer",
|
||||
"version": "3.8.0",
|
||||
"version": "3.9.6",
|
||||
"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",
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
"owner": "ESP32Async",
|
||||
"name": "AsyncTCP",
|
||||
"version": "^3.4.7",
|
||||
"version": "^3.4.10",
|
||||
"platforms": [
|
||||
"espressif32",
|
||||
"libretiny"
|
||||
@@ -1,6 +1,6 @@
|
||||
name=ESP Async WebServer
|
||||
includes=ESPAsyncWebServer.h
|
||||
version=3.8.0
|
||||
version=3.9.6
|
||||
author=ESP32Async
|
||||
maintainer=ESP32Async
|
||||
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
/dependencies.lock
|
||||
/.dummy
|
||||
/managed_components
|
||||
/src/idf_component.yml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[env]
|
||||
framework = arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
||||
build_flags =
|
||||
-Og
|
||||
-Wall -Wextra
|
||||
@@ -17,8 +17,21 @@ monitor_filters = esp32_exception_decoder, log2file
|
||||
lib_compat_mode = strict
|
||||
lib_ldf_mode = chain
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.4.7
|
||||
ESP32Async/ESpAsyncWebServer @ 3.7.0
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
ESP32Async/ESpAsyncWebServer @ 3.9.5
|
||||
custom_component_remove =
|
||||
espressif/esp_hosted
|
||||
espressif/esp_wifi_remote
|
||||
espressif/esp-dsp
|
||||
espressif/esp32-camera
|
||||
espressif/libsodium
|
||||
espressif/esp-modbus
|
||||
espressif/qrcode
|
||||
espressif/esp_insights
|
||||
espressif/esp_diag_data_store
|
||||
espressif/esp_diagnostics
|
||||
espressif/esp_rainmaker
|
||||
espressif/rmaker_common
|
||||
|
||||
custom_sdkconfig = CONFIG_LWIP_MAX_ACTIVE_TCP=32
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// This example demonstrates how to increase the maximum number of active TCP connections
|
||||
|
||||
124
platformio.ini
124
platformio.ini
@@ -14,6 +14,7 @@ lib_dir = .
|
||||
; src_dir = examples/FlashResponse
|
||||
; src_dir = examples/HeaderManipulation
|
||||
; src_dir = examples/Json
|
||||
; src_dir = examples/LargeResponse
|
||||
; src_dir = examples/Logging
|
||||
; src_dir = examples/MessagePack
|
||||
; src_dir = examples/Middleware
|
||||
@@ -33,12 +34,15 @@ src_dir = examples/PerfTests
|
||||
; src_dir = examples/StaticFile
|
||||
; src_dir = examples/Templates
|
||||
; src_dir = examples/Upload
|
||||
; src_dir = examples/UploadFlash
|
||||
; src_dir = examples/URIMatcher
|
||||
; src_dir = examples/URIMatcherTest
|
||||
; src_dir = examples/WebSocket
|
||||
; src_dir = examples/WebSocketEasy
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_flags =
|
||||
-Og
|
||||
@@ -51,7 +55,14 @@ build_flags =
|
||||
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=64
|
||||
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1
|
||||
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096
|
||||
; -D ASYNCWEBSERVER_REGEX=1
|
||||
; -D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||
; -D CONFIG_ARDUHAL_LOG_COLORS
|
||||
; -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
|
||||
; -D USE_ESP_IDF_LOG=1
|
||||
; -D TAG=\"core\"
|
||||
; -D LOG_LOCAL_LEVEL=ESP_LOG_VERBOSE
|
||||
; -D ASYNCWEBSERVER_LOG_DEBUG
|
||||
upload_protocol = esptool
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder, log2file
|
||||
@@ -60,10 +71,14 @@ lib_compat_mode = strict
|
||||
lib_ldf_mode = chain
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson @ 7.4.2
|
||||
ESP32Async/AsyncTCP @ 3.4.7
|
||||
; bblanchon/ArduinoJson @ 6.21.5
|
||||
; bblanchon/ArduinoJson @ 5.13.4
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
board_build.partitions = partitions-4MB.csv
|
||||
board_build.filesystem = littlefs
|
||||
|
||||
; PLATFORMS (ESP32, ESP8266, Raspberry, LibreTiny)
|
||||
|
||||
[env:arduino-2]
|
||||
platform = espressif32@6.12.0
|
||||
|
||||
@@ -71,26 +86,6 @@ platform = espressif32@6.12.0
|
||||
; board = esp32-p4
|
||||
; board = esp32-h2-devkitm-1
|
||||
|
||||
[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.4.7
|
||||
|
||||
[env:arduino-rc-asynctcp]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCP
|
||||
|
||||
[env:arduino-3-no-chunk-inflight]
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0
|
||||
|
||||
[env:AsyncTCPSock]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266
|
||||
; board = huzzah
|
||||
@@ -122,7 +117,42 @@ lib_deps =
|
||||
; (BK7231 already uses it)
|
||||
custom_versions.freertos = 9.0.0
|
||||
|
||||
; CI
|
||||
; SPECIFIC ENVS (for testing various configurations)
|
||||
|
||||
[env:arduino-2-esp-idf-log]
|
||||
platform = espressif32@6.12.0
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D USE_ESP_IDF_LOG=1
|
||||
-D TAG=\"core\"
|
||||
|
||||
[env:arduino-3-esp-idf-log]
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D USE_ESP_IDF_LOG=1
|
||||
|
||||
[env:no-json]
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
|
||||
[env:latest-asynctcp]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCP
|
||||
|
||||
[env:no-chunk-inflight]
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0
|
||||
|
||||
[env:regex]
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_REGEX=1
|
||||
|
||||
[env:AsyncTCPSock]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
; PLATFORM CI (ESP32, ESP8266, Raspberry, LibreTiny)
|
||||
|
||||
[env:ci-arduino-2]
|
||||
platform = espressif32@6.12.0
|
||||
@@ -131,24 +161,6 @@ board = ${sysenv.PIO_BOARD}
|
||||
[env:ci-arduino-3]
|
||||
board = ${sysenv.PIO_BOARD}
|
||||
|
||||
[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.4.7
|
||||
|
||||
[env:ci-arduino-rc-asynctcp]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCP
|
||||
|
||||
[env:ci-arduino-3-no-chunk-inflight]
|
||||
board = ${sysenv.PIO_BOARD}
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=1
|
||||
|
||||
[env:ci-esp8266]
|
||||
platform = espressif8266
|
||||
board = ${sysenv.PIO_BOARD}
|
||||
@@ -177,3 +189,33 @@ lib_deps =
|
||||
DNSServer
|
||||
ESP32Async/AsyncTCP @ 3.4.3
|
||||
custom_versions.freertos = 9.0.0
|
||||
|
||||
; CI FOR SPECIFIC CONFIGURATIONS
|
||||
|
||||
[env:ci-arduino-2-esp-idf-log]
|
||||
platform = espressif32@6.12.0
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D USE_ESP_IDF_LOG=1
|
||||
-D TAG=\"core\"
|
||||
|
||||
[env:ci-arduino-3-esp-idf-log]
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-D USE_ESP_IDF_LOG=1
|
||||
|
||||
[env:ci-no-json]
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.4.10
|
||||
|
||||
[env:ci-latest-asynctcp]
|
||||
lib_deps =
|
||||
https://github.com/ESP32Async/AsyncTCP
|
||||
|
||||
[env:ci-no-chunk-inflight]
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=1
|
||||
|
||||
[env:ci-regex]
|
||||
build_flags = ${env.build_flags}
|
||||
-D ASYNCWEBSERVER_REGEX=1
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "Arduino.h"
|
||||
#if defined(ESP32)
|
||||
#include <rom/ets_sys.h>
|
||||
#endif
|
||||
#include "AsyncEventSource.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa
|
||||
|
||||
@@ -25,9 +26,7 @@ static String generateEventMessage(const char *message, const char *event, uint3
|
||||
len += 42; // give it some overhead
|
||||
|
||||
if (!str.reserve(len)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
@@ -148,7 +147,7 @@ size_t AsyncEventSourceMessage::send(AsyncClient *client) {
|
||||
|
||||
// Client
|
||||
|
||||
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->client()), _server(server) {
|
||||
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->clientRelease()), _server(server) {
|
||||
|
||||
if (request->hasHeader(T_Last_Event_ID)) {
|
||||
_lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str());
|
||||
@@ -186,9 +185,9 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
|
||||
);
|
||||
|
||||
_server->_addClient(this);
|
||||
delete request;
|
||||
|
||||
_client->setNoDelay(true);
|
||||
// delete AsyncWebServerRequest object (and bound response) since we have the ownership on client connection now
|
||||
delete request;
|
||||
}
|
||||
|
||||
AsyncEventSourceClient::~AsyncEventSourceClient() {
|
||||
@@ -201,11 +200,7 @@ AsyncEventSourceClient::~AsyncEventSourceClient() {
|
||||
|
||||
bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
|
||||
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
|
||||
#ifdef ESP8266
|
||||
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
|
||||
#elif defined(ESP32)
|
||||
log_e("Event message queue overflow: discard message");
|
||||
#endif
|
||||
async_ws_log_e("Event message queue overflow: discard message");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -214,7 +209,12 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_lockmq);
|
||||
#endif
|
||||
|
||||
_messageQueue.emplace_back(message, len);
|
||||
if (_client) {
|
||||
_messageQueue.emplace_back(message, len);
|
||||
} else {
|
||||
_messageQueue.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
throttle queue run
|
||||
@@ -222,7 +222,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
|
||||
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
|
||||
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
|
||||
*/
|
||||
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
|
||||
if (_client && _client->canSend() && _messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2) {
|
||||
_runQueue();
|
||||
}
|
||||
|
||||
@@ -231,11 +231,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
|
||||
|
||||
bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
|
||||
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
|
||||
#ifdef ESP8266
|
||||
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
|
||||
#elif defined(ESP32)
|
||||
log_e("Event message queue overflow: discard message");
|
||||
#endif
|
||||
async_ws_log_e("Event message queue overflow: discard message");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -244,7 +240,12 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_lockmq);
|
||||
#endif
|
||||
|
||||
_messageQueue.emplace_back(std::move(msg));
|
||||
if (_client) {
|
||||
_messageQueue.emplace_back(std::move(msg));
|
||||
} else {
|
||||
_messageQueue.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
throttle queue run
|
||||
@@ -252,7 +253,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
|
||||
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
|
||||
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
|
||||
*/
|
||||
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
|
||||
if (_client && _client->canSend() && _messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2) {
|
||||
_runQueue();
|
||||
}
|
||||
return true;
|
||||
@@ -298,7 +299,7 @@ void AsyncEventSourceClient::_onPoll() {
|
||||
|
||||
void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))) {
|
||||
if (_client) {
|
||||
_client->close(true);
|
||||
_client->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +344,7 @@ void AsyncEventSourceClient::_runQueue() {
|
||||
}
|
||||
|
||||
// flush socket
|
||||
if (total_bytes_written) {
|
||||
if (_client && total_bytes_written) {
|
||||
_client->send();
|
||||
}
|
||||
}
|
||||
@@ -419,17 +420,13 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
|
||||
#ifdef ESP32
|
||||
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
|
||||
#endif
|
||||
if (!_clients.size()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const auto &c : _clients) {
|
||||
if (c->connected()) {
|
||||
aql += c->packetsWaiting();
|
||||
++nConnectedClients;
|
||||
}
|
||||
}
|
||||
return ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up
|
||||
return nConnectedClients == 0 ? 0 : ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up
|
||||
}
|
||||
|
||||
AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
|
||||
@@ -440,10 +437,12 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c
|
||||
size_t hits = 0;
|
||||
size_t miss = 0;
|
||||
for (const auto &c : _clients) {
|
||||
if (c->write(shared_msg)) {
|
||||
++hits;
|
||||
} else {
|
||||
++miss;
|
||||
if (c->connected()) {
|
||||
if (c->write(shared_msg)) {
|
||||
++hits;
|
||||
} else {
|
||||
++miss;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED);
|
||||
@@ -471,11 +470,15 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
request->send(new AsyncEventSourceResponse(this));
|
||||
}
|
||||
|
||||
// list iteration protected by caller's lock
|
||||
void AsyncEventSource::_adjust_inflight_window() {
|
||||
if (_clients.size()) {
|
||||
size_t inflight = SSE_MAX_INFLIGH / _clients.size();
|
||||
const size_t clientCount = count();
|
||||
if (clientCount) {
|
||||
size_t inflight = SSE_MAX_INFLIGH / clientCount;
|
||||
for (const auto &c : _clients) {
|
||||
c->set_max_inflight_bytes(inflight);
|
||||
if (c->connected()) {
|
||||
c->set_max_inflight_bytes(inflight);
|
||||
}
|
||||
}
|
||||
// Serial.printf("adjusted inflight to: %u\n", inflight);
|
||||
}
|
||||
@@ -483,8 +486,7 @@ void AsyncEventSource::_adjust_inflight_window() {
|
||||
|
||||
/* Response */
|
||||
|
||||
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
|
||||
_server = server;
|
||||
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) : _server(server) {
|
||||
_code = 200;
|
||||
_contentType = T_text_event_stream;
|
||||
_sendContentLength = false;
|
||||
@@ -495,13 +497,24 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
|
||||
void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request) {
|
||||
String out;
|
||||
_assembleHead(out, request->version());
|
||||
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
|
||||
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
|
||||
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
|
||||
_request = request;
|
||||
request->client()->onAck(
|
||||
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
|
||||
if (len) {
|
||||
static_cast<AsyncEventSourceResponse *>(r)->_switchClient();
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
request->client()->write(out.c_str(), _headLength);
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
}
|
||||
|
||||
size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time __attribute__((unused))) {
|
||||
if (len) {
|
||||
new AsyncEventSourceClient(request, _server);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void AsyncEventSourceResponse::_switchClient() {
|
||||
// AsyncEventSourceClient c-tor will take the ownership of AsyncTCP's client connection
|
||||
new AsyncEventSourceClient(_request, _server);
|
||||
// AsyncEventSourceClient c-tor would also delete _request and *this
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef ASYNCEVENTSOURCE_H_
|
||||
#define ASYNCEVENTSOURCE_H_
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -44,6 +43,10 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
class AsyncEventSource;
|
||||
class AsyncEventSourceResponse;
|
||||
class AsyncEventSourceClient;
|
||||
@@ -141,6 +144,13 @@ private:
|
||||
void _runQueue();
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Async Event Source Client object
|
||||
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
|
||||
*
|
||||
* @param request
|
||||
* @param server
|
||||
*/
|
||||
AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server);
|
||||
~AsyncEventSourceClient();
|
||||
|
||||
@@ -305,21 +315,24 @@ public:
|
||||
// system callbacks (do not call from user code!)
|
||||
void _addClient(AsyncEventSourceClient *client);
|
||||
void _handleDisconnect(AsyncEventSourceClient *client);
|
||||
bool canHandle(AsyncWebServerRequest *request) const override final;
|
||||
void handleRequest(AsyncWebServerRequest *request) override final;
|
||||
bool canHandle(AsyncWebServerRequest *request) const final;
|
||||
void handleRequest(AsyncWebServerRequest *request) final;
|
||||
};
|
||||
|
||||
class AsyncEventSourceResponse : public AsyncWebServerResponse {
|
||||
private:
|
||||
AsyncEventSource *_server;
|
||||
AsyncWebServerRequest *_request;
|
||||
// this call back will switch AsyncTCP client to SSE
|
||||
void _switchClient();
|
||||
|
||||
public:
|
||||
AsyncEventSourceResponse(AsyncEventSource *server);
|
||||
void _respond(AsyncWebServerRequest *request);
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
|
||||
return 0;
|
||||
};
|
||||
bool _sourceValid() const {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* ASYNCEVENTSOURCE_H_ */
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "AsyncJson.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
|
||||
// Json content type response classes
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 5
|
||||
AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} {
|
||||
_code = 200;
|
||||
@@ -56,7 +61,7 @@ size_t AsyncJsonResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
#else
|
||||
serializeJson(_root, dest);
|
||||
#endif
|
||||
return len;
|
||||
return dest.written();
|
||||
}
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
@@ -84,15 +89,37 @@ size_t PrettyAsyncJsonResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
#else
|
||||
serializeJsonPretty(_root, dest);
|
||||
#endif
|
||||
return len;
|
||||
return dest.written();
|
||||
}
|
||||
|
||||
// MessagePack content type response
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
|
||||
size_t AsyncMessagePackResponse::setLength() {
|
||||
_contentLength = measureMsgPack(_root);
|
||||
if (_contentLength) {
|
||||
_isValid = true;
|
||||
}
|
||||
return _contentLength;
|
||||
}
|
||||
|
||||
size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
ChunkPrint dest(data, _sentLength, len);
|
||||
serializeMsgPack(_root, dest);
|
||||
return dest.written();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Body handler supporting both content types: JSON and MessagePack
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
|
||||
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
|
||||
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
|
||||
: _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize),
|
||||
_maxContentLength(16384) {}
|
||||
#else
|
||||
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest)
|
||||
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
|
||||
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest)
|
||||
: _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
|
||||
#endif
|
||||
|
||||
bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) const {
|
||||
@@ -100,15 +127,16 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
|
||||
if (!_uri.matches(request)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_json)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
return request->method() == HTTP_GET || request->contentType().equalsIgnoreCase(asyncsrv::T_application_json)
|
||||
|| request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack);
|
||||
#else
|
||||
return request->method() == HTTP_GET || request->contentType().equalsIgnoreCase(asyncsrv::T_application_json);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
@@ -123,9 +151,7 @@ void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request)
|
||||
// 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
|
||||
async_ws_log_e("Content length exceeds maximum allowed");
|
||||
request->send(413);
|
||||
return;
|
||||
}
|
||||
@@ -137,26 +163,32 @@ void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request)
|
||||
}
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 5
|
||||
DynamicJsonBuffer jsonBuffer;
|
||||
JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
|
||||
if (json.success()) {
|
||||
DynamicJsonBuffer doc;
|
||||
#elif ARDUINOJSON_VERSION_MAJOR == 6
|
||||
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
|
||||
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
DynamicJsonDocument doc(this->maxJsonBufferSize);
|
||||
#else
|
||||
JsonDocument jsonBuffer;
|
||||
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
JsonDocument doc;
|
||||
#endif
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 5
|
||||
JsonVariant json = doc.parse((const char *)request->_tempObject);
|
||||
if (json.success()) {
|
||||
_onRequest(request, json);
|
||||
} else {
|
||||
// error parsing the body
|
||||
request->send(400);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
DeserializationError error = request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)
|
||||
? deserializeMsgPack(doc, (uint8_t *)(request->_tempObject))
|
||||
: deserializeJson(doc, (const char *)request->_tempObject);
|
||||
if (!error) {
|
||||
JsonVariant json = doc.as<JsonVariant>();
|
||||
_onRequest(request, json);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// error parsing the body
|
||||
request->send(400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +204,7 @@ void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uin
|
||||
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");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef ASYNC_JSON_H_
|
||||
#define ASYNC_JSON_H_
|
||||
#pragma once
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#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")
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "ChunkPrint.h"
|
||||
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include "ChunkPrint.h"
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
|
||||
@@ -24,6 +14,8 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Json content type response classes
|
||||
|
||||
class AsyncJsonResponse : public AsyncAbstractResponse {
|
||||
protected:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 5
|
||||
@@ -49,11 +41,11 @@ public:
|
||||
bool _sourceValid() const {
|
||||
return _isValid;
|
||||
}
|
||||
size_t setLength();
|
||||
virtual size_t setLength();
|
||||
size_t getSize() const {
|
||||
return _jsonBuffer.size();
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *data, size_t len);
|
||||
virtual size_t _fillBuffer(uint8_t *data, size_t len);
|
||||
#if ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
bool overflowed() const {
|
||||
return _jsonBuffer.overflowed();
|
||||
@@ -68,15 +60,35 @@ public:
|
||||
#else
|
||||
PrettyAsyncJsonResponse(bool isArray = false);
|
||||
#endif
|
||||
size_t setLength();
|
||||
size_t _fillBuffer(uint8_t *data, size_t len);
|
||||
size_t setLength() override;
|
||||
size_t _fillBuffer(uint8_t *data, size_t len) override;
|
||||
};
|
||||
|
||||
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction;
|
||||
// MessagePack content type response
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
|
||||
class AsyncMessagePackResponse : public AsyncJsonResponse {
|
||||
public:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse(isArray, maxJsonBufferSize) {
|
||||
_contentType = asyncsrv::T_application_msgpack;
|
||||
}
|
||||
#else
|
||||
AsyncMessagePackResponse(bool isArray = false) : AsyncJsonResponse(isArray) {
|
||||
_contentType = asyncsrv::T_application_msgpack;
|
||||
}
|
||||
#endif
|
||||
size_t setLength() override;
|
||||
size_t _fillBuffer(uint8_t *data, size_t len) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
// Body handler supporting both content types: JSON and MessagePack
|
||||
|
||||
class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
|
||||
protected:
|
||||
String _uri;
|
||||
AsyncURIMatcher _uri;
|
||||
WebRequestMethodComposite _method;
|
||||
ArJsonRequestHandlerFunction _onRequest;
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
@@ -86,9 +98,9 @@ protected:
|
||||
|
||||
public:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
#else
|
||||
AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr);
|
||||
AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr);
|
||||
#endif
|
||||
|
||||
void setMethod(WebRequestMethodComposite method) {
|
||||
@@ -101,18 +113,16 @@ public:
|
||||
_onRequest = fn;
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) const override final;
|
||||
void handleRequest(AsyncWebServerRequest *request) override final;
|
||||
bool canHandle(AsyncWebServerRequest *request) const final;
|
||||
void handleRequest(AsyncWebServerRequest *request) final;
|
||||
void handleUpload(
|
||||
__unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len,
|
||||
__unused bool final
|
||||
) override final {}
|
||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
|
||||
bool isRequestHandlerTrivial() const override final {
|
||||
) final {}
|
||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) final;
|
||||
bool isRequestHandlerTrivial() const final {
|
||||
return !_onRequest;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // ASYNC_JSON_SUPPORT == 1
|
||||
|
||||
#endif // ASYNC_JSON_H_
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "AsyncMessagePack.h"
|
||||
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
|
||||
_code = 200;
|
||||
_contentType = asyncsrv::T_application_msgpack;
|
||||
if (isArray) {
|
||||
_root = _jsonBuffer.createNestedArray();
|
||||
} else {
|
||||
_root = _jsonBuffer.createNestedObject();
|
||||
}
|
||||
}
|
||||
#else
|
||||
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray) : _isValid{false} {
|
||||
_code = 200;
|
||||
_contentType = asyncsrv::T_application_msgpack;
|
||||
if (isArray) {
|
||||
_root = _jsonBuffer.add<JsonArray>();
|
||||
} else {
|
||||
_root = _jsonBuffer.add<JsonObject>();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t AsyncMessagePackResponse::setLength() {
|
||||
_contentLength = measureMsgPack(_root);
|
||||
if (_contentLength) {
|
||||
_isValid = true;
|
||||
}
|
||||
return _contentLength;
|
||||
}
|
||||
|
||||
size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
ChunkPrint dest(data, _sentLength, len);
|
||||
serializeMsgPack(_root, dest);
|
||||
return len;
|
||||
}
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(
|
||||
const String &uri, ArMessagePackRequestHandlerFunction onRequest, size_t maxJsonBufferSize
|
||||
)
|
||||
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
|
||||
#else
|
||||
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest)
|
||||
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
|
||||
#endif
|
||||
|
||||
bool AsyncCallbackMessagePackWebHandler::canHandle(AsyncWebServerRequest *request) const {
|
||||
if (!_onRequest || !request->isHTTP() || !(_method & request->method())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncCallbackMessagePackWebHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
if (_onRequest) {
|
||||
if (request->method() == HTTP_GET) {
|
||||
JsonVariant json;
|
||||
_onRequest(request, json);
|
||||
return;
|
||||
} else if (request->_tempObject != NULL) {
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
|
||||
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject));
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#else
|
||||
JsonDocument jsonBuffer;
|
||||
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject));
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#endif
|
||||
|
||||
_onRequest(request, json);
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(_contentLength > _maxContentLength ? 413 : 400);
|
||||
} else {
|
||||
request->send(500);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncCallbackMessagePackWebHandler::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);
|
||||
if (request->_tempObject == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (request->_tempObject != NULL) {
|
||||
memcpy((uint8_t *)(request->_tempObject) + index, data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ASYNC_MSG_PACK_SUPPORT
|
||||
@@ -1,126 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
server.on("/msg_pack", HTTP_ANY, [](AsyncWebServerRequest * request) {
|
||||
AsyncMessagePackResponse * response = new AsyncMessagePackResponse();
|
||||
JsonObject& root = response->getRoot();
|
||||
root["key1"] = "key number one";
|
||||
JsonObject& nested = root.createNestedObject("nested");
|
||||
nested["key1"] = "key number one";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
#warning "AsyncMessagePack.h is deprecated, just include ESPAsyncWebServer.h from now on"
|
||||
|
||||
--------------------
|
||||
|
||||
AsyncCallbackMessagePackWebHandler* handler = new AsyncCallbackMessagePackWebHandler("/msg_pack/endpoint");
|
||||
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
JsonObject jsonObj = json.as<JsonObject>();
|
||||
// ...
|
||||
});
|
||||
server.addHandler(handler);
|
||||
*/
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#if ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
#define ASYNC_MSG_PACK_SUPPORT 1
|
||||
#else
|
||||
#define ASYNC_MSG_PACK_SUPPORT 0
|
||||
#endif // ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
#endif // __has_include("ArduinoJson.h")
|
||||
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include "ChunkPrint.h"
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
|
||||
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
|
||||
#endif
|
||||
#endif
|
||||
|
||||
class AsyncMessagePackResponse : public AsyncAbstractResponse {
|
||||
protected:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
DynamicJsonDocument _jsonBuffer;
|
||||
#else
|
||||
JsonDocument _jsonBuffer;
|
||||
#endif
|
||||
|
||||
JsonVariant _root;
|
||||
bool _isValid;
|
||||
|
||||
public:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
#else
|
||||
AsyncMessagePackResponse(bool isArray = false);
|
||||
#endif
|
||||
JsonVariant &getRoot() {
|
||||
return _root;
|
||||
}
|
||||
bool _sourceValid() const {
|
||||
return _isValid;
|
||||
}
|
||||
size_t setLength();
|
||||
size_t getSize() const {
|
||||
return _jsonBuffer.size();
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *data, size_t len);
|
||||
#if ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
bool overflowed() const {
|
||||
return _jsonBuffer.overflowed();
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArMessagePackRequestHandlerFunction;
|
||||
|
||||
class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler {
|
||||
protected:
|
||||
String _uri;
|
||||
WebRequestMethodComposite _method;
|
||||
ArMessagePackRequestHandlerFunction _onRequest;
|
||||
size_t _contentLength;
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
size_t maxJsonBufferSize;
|
||||
#endif
|
||||
size_t _maxContentLength;
|
||||
|
||||
public:
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
AsyncCallbackMessagePackWebHandler(
|
||||
const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE
|
||||
);
|
||||
#else
|
||||
AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr);
|
||||
#endif
|
||||
|
||||
void setMethod(WebRequestMethodComposite method) {
|
||||
_method = method;
|
||||
}
|
||||
void setMaxContentLength(int maxContentLength) {
|
||||
_maxContentLength = maxContentLength;
|
||||
}
|
||||
void onRequest(ArMessagePackRequestHandlerFunction fn) {
|
||||
_onRequest = fn;
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) const override final;
|
||||
void handleRequest(AsyncWebServerRequest *request) override final;
|
||||
void handleUpload(
|
||||
__unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len,
|
||||
__unused bool final
|
||||
) override final {}
|
||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
|
||||
bool isRequestHandlerTrivial() const override final {
|
||||
return !_onRequest;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // ASYNC_MSG_PACK_SUPPORT == 1
|
||||
#include "AsyncJson.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
|
||||
175
src/AsyncWebServerLogging.h
Normal file
175
src/AsyncWebServerLogging.h
Normal file
@@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef ASYNCWEBSERVER_LOG_CUSTOM
|
||||
// The user must provide the following macros in AsyncWebServerLoggingCustom.h:
|
||||
// async_ws_log_e, async_ws_log_w, async_ws_log_i, async_ws_log_d, async_ws_log_v
|
||||
#include <AsyncWebServerLoggingCustom.h>
|
||||
|
||||
#elif defined(ASYNCWEBSERVER_LOG_DEBUG)
|
||||
// Local Debug logging
|
||||
#include <HardwareSerial.h>
|
||||
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
|
||||
#else
|
||||
// Framework-based logging
|
||||
|
||||
/**
|
||||
* LibreTiny specific configurations
|
||||
*/
|
||||
#if defined(LIBRETINY)
|
||||
#include <Arduino.h>
|
||||
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
|
||||
|
||||
/**
|
||||
* Raspberry Pi Pico specific configurations
|
||||
*/
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <HardwareSerial.h>
|
||||
// define log levels
|
||||
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
|
||||
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
|
||||
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
|
||||
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
|
||||
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
|
||||
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
|
||||
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
|
||||
// set default log level
|
||||
#ifndef ASYNCWEBSERVER_LOG_LEVEL
|
||||
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
|
||||
#endif
|
||||
// error
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
|
||||
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_e(format, ...)
|
||||
#endif
|
||||
// warn
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
|
||||
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_w(format, ...)
|
||||
#endif
|
||||
// info
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
|
||||
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_i(format, ...)
|
||||
#endif
|
||||
// debug
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
|
||||
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_d(format, ...)
|
||||
#endif
|
||||
// verbose
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
|
||||
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_v(format, ...)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* ESP8266 specific configurations
|
||||
* Uses ets_printf to avoid dependency on global Serial object.
|
||||
* Format strings are stored in PROGMEM and copied to a stack buffer.
|
||||
*/
|
||||
#elif defined(ESP8266)
|
||||
#include <ets_sys.h>
|
||||
#include <pgmspace.h>
|
||||
// define log levels
|
||||
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
|
||||
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
|
||||
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
|
||||
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
|
||||
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
|
||||
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
|
||||
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
|
||||
// set default log level
|
||||
#ifndef ASYNCWEBSERVER_LOG_LEVEL
|
||||
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
|
||||
#endif
|
||||
// helper macro to copy PROGMEM format string to stack and call ets_printf
|
||||
// level is a char literal ('E', 'W', etc.) to avoid RAM usage from string literals
|
||||
#define _ASYNC_WS_LOG(level, format, ...) \
|
||||
do { \
|
||||
static const char __fmt[] PROGMEM = "%c async_ws %d: " format "\n"; \
|
||||
char __buf[sizeof(__fmt)]; \
|
||||
strcpy_P(__buf, __fmt); \
|
||||
ets_printf(__buf, level, __LINE__, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
// error
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
|
||||
#define async_ws_log_e(format, ...) _ASYNC_WS_LOG('E', format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define async_ws_log_e(format, ...)
|
||||
#endif
|
||||
// warn
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
|
||||
#define async_ws_log_w(format, ...) _ASYNC_WS_LOG('W', format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define async_ws_log_w(format, ...)
|
||||
#endif
|
||||
// info
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
|
||||
#define async_ws_log_i(format, ...) _ASYNC_WS_LOG('I', format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define async_ws_log_i(format, ...)
|
||||
#endif
|
||||
// debug
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
|
||||
#define async_ws_log_d(format, ...) _ASYNC_WS_LOG('D', format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define async_ws_log_d(format, ...)
|
||||
#endif
|
||||
// verbose
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
|
||||
#define async_ws_log_v(format, ...) _ASYNC_WS_LOG('V', format, ##__VA_ARGS__)
|
||||
#else
|
||||
#define async_ws_log_v(format, ...)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Arduino specific configurations
|
||||
*/
|
||||
#elif defined(ARDUINO)
|
||||
#if defined(USE_ESP_IDF_LOG)
|
||||
#include <esp_log.h>
|
||||
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
|
||||
#else
|
||||
#include <esp32-hal-log.h>
|
||||
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
|
||||
#endif // USE_ESP_IDF_LOG
|
||||
|
||||
/**
|
||||
* ESP-IDF specific configurations
|
||||
*/
|
||||
#else
|
||||
#include <esp_log.h>
|
||||
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#endif // !LIBRETINY && !ARDUINO
|
||||
|
||||
#endif // ASYNCWEBSERVER_LOG_CUSTOM
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
/**
|
||||
@@ -29,20 +32,16 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
|
||||
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);
|
||||
char serverETag[11];
|
||||
if (!_getEtag(gzFile, serverETag)) {
|
||||
// Compressed file not found or invalid
|
||||
send(404);
|
||||
gzFile.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare with client's ETag
|
||||
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
|
||||
@@ -59,27 +58,39 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generates an ETag string from a 4-byte trailer
|
||||
* @brief Generates an ETag string (enclosed into quotes) from the CRC32 trailer of a GZIP file.
|
||||
*
|
||||
* This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes.
|
||||
* This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file
|
||||
* and converts it into an 8-character hexadecimal ETag string (enclosed in double quotes and null-terminated).
|
||||
* Double quotes for ETag value are required by RFC9110 section 8.8.3.
|
||||
*
|
||||
* @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)
|
||||
* @param gzFile Opened file handle pointing to the GZIP file.
|
||||
* @param eTag Output buffer to store the generated ETag.
|
||||
* Must be pre-allocated with at least 11 bytes (8 for hex digits + 2 for quotes + 1 for null terminator).
|
||||
*
|
||||
* @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed).
|
||||
*/
|
||||
void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) {
|
||||
bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) {
|
||||
static constexpr char hexChars[] = "0123456789ABCDEF";
|
||||
|
||||
uint32_t data;
|
||||
memcpy(&data, trailer, 4);
|
||||
if (!gzFile.seek(gzFile.size() - 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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';
|
||||
uint32_t crc;
|
||||
gzFile.read(reinterpret_cast<uint8_t *>(&crc), sizeof(crc));
|
||||
|
||||
etag[0] = '"';
|
||||
etag[1] = hexChars[(crc >> 4) & 0x0F];
|
||||
etag[2] = hexChars[crc & 0x0F];
|
||||
etag[3] = hexChars[(crc >> 12) & 0x0F];
|
||||
etag[4] = hexChars[(crc >> 8) & 0x0F];
|
||||
etag[5] = hexChars[(crc >> 20) & 0x0F];
|
||||
etag[6] = hexChars[(crc >> 16) & 0x0F];
|
||||
etag[7] = hexChars[(crc >> 28)];
|
||||
etag[8] = hexChars[(crc >> 24) & 0x0F];
|
||||
etag[9] = '"';
|
||||
etag[10] = '\0';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -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 8
|
||||
#define ASYNCWEBSERVER_VERSION_MINOR 9
|
||||
/** Patch version number (x.x.X) */
|
||||
#define ASYNCWEBSERVER_VERSION_PATCH 0
|
||||
#define ASYNCWEBSERVER_VERSION_PATCH 6
|
||||
|
||||
/**
|
||||
* Macro to convert version number into an integer
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "AsyncWebSocket.h"
|
||||
#include "Arduino.h"
|
||||
|
||||
#include <cstring>
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <libb64/cencode.h>
|
||||
|
||||
@@ -21,6 +19,12 @@
|
||||
#include <mbedtls/sha1.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
size_t webSocketSendFrameWindow(AsyncClient *client) {
|
||||
@@ -48,10 +52,10 @@ size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool
|
||||
uint8_t headLen = 2;
|
||||
if (len && mask) {
|
||||
headLen += 4;
|
||||
mbuf[0] = rand() % 0xFF;
|
||||
mbuf[1] = rand() % 0xFF;
|
||||
mbuf[2] = rand() % 0xFF;
|
||||
mbuf[3] = rand() % 0xFF;
|
||||
mbuf[0] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
|
||||
mbuf[1] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
|
||||
mbuf[2] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
|
||||
mbuf[3] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
|
||||
}
|
||||
if (len > 125) {
|
||||
headLen += 2;
|
||||
@@ -68,9 +72,7 @@ size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(headLen);
|
||||
if (buf == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
client->abort();
|
||||
return 0;
|
||||
}
|
||||
@@ -223,14 +225,10 @@ size_t AsyncWebSocketMessage::send(AsyncClient *client) {
|
||||
const char *AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING";
|
||||
const size_t AWSC_PING_PAYLOAD_LEN = 22;
|
||||
|
||||
AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : _tempObject(NULL) {
|
||||
_client = request->client();
|
||||
_server = server;
|
||||
_clientId = _server->_getNextId();
|
||||
_status = WS_CONNECTED;
|
||||
_pstate = 0;
|
||||
_lastMessageTime = millis();
|
||||
_keepAlivePeriod = 0;
|
||||
AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server)
|
||||
: _client(client), _server(server), _clientId(_server->_getNextId()), _status(WS_CONNECTED), _pstate(0), _lastMessageTime(millis()), _keepAlivePeriod(0),
|
||||
_tempObject(NULL) {
|
||||
|
||||
_client->setRxTimeout(0);
|
||||
_client->onError(
|
||||
[](void *r, AsyncClient *c, int8_t error) {
|
||||
@@ -274,7 +272,6 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
|
||||
},
|
||||
this
|
||||
);
|
||||
delete request;
|
||||
memset(&_pinfo, 0, sizeof(_pinfo));
|
||||
}
|
||||
|
||||
@@ -313,12 +310,12 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
|
||||
#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()
|
||||
Due to _client->close() shall call the callback function _onDisconnect()
|
||||
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
|
||||
*/
|
||||
lock.unlock();
|
||||
#endif
|
||||
_client->close(true);
|
||||
_client->close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -425,26 +422,18 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
|
||||
#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()
|
||||
Due to _client->close() shall call the callback function _onDisconnect()
|
||||
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
|
||||
*/
|
||||
lock.unlock();
|
||||
#endif
|
||||
_client->close(true);
|
||||
_client->close();
|
||||
}
|
||||
|
||||
#ifdef ESP8266
|
||||
ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: closing connection\n");
|
||||
#elif defined(ESP32)
|
||||
log_e("Too many messages queued: closing connection");
|
||||
#endif
|
||||
async_ws_log_e("Too many messages queued: closing connection");
|
||||
|
||||
} else {
|
||||
#ifdef ESP8266
|
||||
ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: discarding new message\n");
|
||||
#elif defined(ESP32)
|
||||
log_e("Too many messages queued: discarding new message");
|
||||
#endif
|
||||
async_ws_log_e("Too many messages queued: discarding new message");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -486,10 +475,8 @@ void AsyncWebSocketClient::close(uint16_t code, const char *message) {
|
||||
free(buf);
|
||||
return;
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
async_ws_log_e("Failed to allocate");
|
||||
_client->abort();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
_queueControl(WS_DISCONNECT);
|
||||
@@ -509,7 +496,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) {
|
||||
}
|
||||
// Serial.println("onTime");
|
||||
(void)time;
|
||||
_client->close(true);
|
||||
_client->close();
|
||||
}
|
||||
|
||||
void AsyncWebSocketClient::_onDisconnect() {
|
||||
@@ -528,12 +515,15 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
|
||||
_pinfo.index = 0;
|
||||
_pinfo.final = (fdata[0] & 0x80) != 0;
|
||||
_pinfo.opcode = fdata[0] & 0x0F;
|
||||
_pinfo.masked = (fdata[1] & 0x80) != 0;
|
||||
_pinfo.masked = ((fdata[1] & 0x80) != 0) ? 1 : 0;
|
||||
_pinfo.len = fdata[1] & 0x7F;
|
||||
|
||||
// log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen);
|
||||
// log_d("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status);
|
||||
// log_d("WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len);
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen);
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status);
|
||||
// async_ws_log_w(
|
||||
// "WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index,
|
||||
// _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len
|
||||
// );
|
||||
|
||||
data += 2;
|
||||
plen -= 2;
|
||||
@@ -549,18 +539,50 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
|
||||
data += 8;
|
||||
plen -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (_pinfo.masked
|
||||
&& plen >= 4) { // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not decrement plen which is already 0.
|
||||
memcpy(_pinfo.mask, data, 4);
|
||||
data += 4;
|
||||
plen -= 4;
|
||||
if (_pinfo.masked) {
|
||||
// Read mask bytes (may be fragmented across packets in Safari)
|
||||
size_t mask_offset = 0;
|
||||
|
||||
// If we're resuming from a previous fragmented read, check _pinfo.index
|
||||
if (_pstate == 1 && _pinfo.index < 4) {
|
||||
mask_offset = _pinfo.index;
|
||||
}
|
||||
|
||||
// Read as many mask bytes as available
|
||||
while (mask_offset < 4 && plen > 0) {
|
||||
_pinfo.mask[mask_offset++] = *data++;
|
||||
plen--;
|
||||
}
|
||||
|
||||
// Check if we have all 4 mask bytes
|
||||
if (mask_offset < 4) {
|
||||
// Incomplete mask
|
||||
if (_pinfo.opcode == WS_DISCONNECT && plen == 0) {
|
||||
// Safari close frame edge case: masked bit set but no mask data
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: close frame with incomplete mask, treating as unmasked", _clientId);
|
||||
_pinfo.masked = 0;
|
||||
_pinfo.index = 0;
|
||||
} else {
|
||||
// Wait for more data
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: waiting for more mask data: read=%zu/4", _clientId, mask_offset);
|
||||
_pinfo.index = mask_offset; // Save progress
|
||||
_pstate = 1;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// All mask bytes received
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: mask complete", _clientId);
|
||||
_pinfo.index = 0; // Reset index for payload processing
|
||||
}
|
||||
}
|
||||
|
||||
const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen);
|
||||
const auto datalast = data[datalen];
|
||||
|
||||
// async_ws_log_w("WS[%" PRIu32 "]: _processing data: datalen=%" PRIu32 ", plen=%" PRIu32, _clientId, datalen, plen);
|
||||
|
||||
if (_pinfo.masked) {
|
||||
for (size_t i = 0; i < datalen; i++) {
|
||||
data[i] ^= _pinfo.mask[(_pinfo.index + i) % 4];
|
||||
@@ -594,7 +616,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
|
||||
if (_status == WS_DISCONNECTING) {
|
||||
_status = WS_DISCONNECTED;
|
||||
if (_client) {
|
||||
_client->close(true);
|
||||
_client->close();
|
||||
}
|
||||
} else {
|
||||
_status = WS_DISCONNECTING;
|
||||
@@ -619,7 +641,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len);
|
||||
// async_ws_log_w("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len);
|
||||
// what should we do?
|
||||
break;
|
||||
}
|
||||
@@ -818,7 +840,10 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType typ
|
||||
|
||||
AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) {
|
||||
_clients.emplace_back(request, this);
|
||||
// we've just detached AsyncTCP client from AsyncWebServerRequest
|
||||
_handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0);
|
||||
// after user code completed CONNECT event callback we can delete req/response objects
|
||||
delete request;
|
||||
return &_clients.back();
|
||||
}
|
||||
|
||||
@@ -1230,9 +1255,7 @@ void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request) {
|
||||
const AsyncWebHeader *key = request->getHeader(WS_STR_KEY);
|
||||
AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), this);
|
||||
if (response == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
@@ -1257,8 +1280,7 @@ AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(const uint8_t *data, siz
|
||||
* Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480
|
||||
*/
|
||||
|
||||
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) {
|
||||
_server = server;
|
||||
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) : _server(server) {
|
||||
_code = 101;
|
||||
_sendContentLength = false;
|
||||
|
||||
@@ -1270,7 +1292,7 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
|
||||
#else
|
||||
String k;
|
||||
if (!k.reserve(key.length() + WS_STR_UUID_LEN)) {
|
||||
log_e("Failed to allocate");
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return;
|
||||
}
|
||||
k.concat(key);
|
||||
@@ -1301,21 +1323,29 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
|
||||
|
||||
void AsyncWebSocketResponse::_respond(AsyncWebServerRequest *request) {
|
||||
if (_state == RESPONSE_FAILED) {
|
||||
request->client()->close(true);
|
||||
request->client()->close();
|
||||
return;
|
||||
}
|
||||
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
|
||||
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
|
||||
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
|
||||
_request = request;
|
||||
request->client()->onAck(
|
||||
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
|
||||
if (len) {
|
||||
static_cast<AsyncWebSocketResponse *>(r)->_switchClient();
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
String out;
|
||||
_assembleHead(out, request->version());
|
||||
request->client()->write(out.c_str(), _headLength);
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
}
|
||||
|
||||
size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
(void)time;
|
||||
|
||||
if (len) {
|
||||
_server->_newClient(request);
|
||||
}
|
||||
|
||||
return 0;
|
||||
void AsyncWebSocketResponse::_switchClient() {
|
||||
// detach client from request
|
||||
_server->_newClient(_request);
|
||||
// _newClient() would also destruct _request and *this
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef ASYNCWEBSOCKET_H_
|
||||
#define ASYNCWEBSOCKET_H_
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -30,7 +29,11 @@
|
||||
#endif
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncWebServerLogging.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#ifdef ESP8266
|
||||
@@ -80,9 +83,7 @@ public:
|
||||
_data = (uint8_t *)malloc(_len);
|
||||
|
||||
if (_data == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
_len = 0;
|
||||
} else {
|
||||
memcpy(_data, data, len);
|
||||
@@ -212,6 +213,9 @@ private:
|
||||
AsyncWebSocket *_server;
|
||||
uint32_t _clientId;
|
||||
AwsClientStatus _status;
|
||||
uint8_t _pstate;
|
||||
uint32_t _lastMessageTime;
|
||||
uint32_t _keepAlivePeriod;
|
||||
#ifdef ESP32
|
||||
mutable std::recursive_mutex _lock;
|
||||
#endif
|
||||
@@ -219,12 +223,8 @@ private:
|
||||
std::deque<AsyncWebSocketMessage> _messageQueue;
|
||||
bool closeWhenFull = true;
|
||||
|
||||
uint8_t _pstate;
|
||||
AwsFrameInfo _pinfo;
|
||||
|
||||
uint32_t _lastMessageTime;
|
||||
uint32_t _keepAlivePeriod;
|
||||
|
||||
bool _queueControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false);
|
||||
bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false);
|
||||
void _runQueue();
|
||||
@@ -233,7 +233,15 @@ private:
|
||||
public:
|
||||
void *_tempObject;
|
||||
|
||||
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server);
|
||||
AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server);
|
||||
|
||||
/**
|
||||
* @brief Construct a new Async Web Socket Client object
|
||||
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
|
||||
* @param request
|
||||
* @param server
|
||||
*/
|
||||
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : AsyncWebSocketClient(request->clientRelease(), server){};
|
||||
~AsyncWebSocketClient();
|
||||
|
||||
// client id increments for the given server
|
||||
@@ -448,8 +456,8 @@ public:
|
||||
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;
|
||||
bool canHandle(AsyncWebServerRequest *request) const final;
|
||||
void handleRequest(AsyncWebServerRequest *request) final;
|
||||
|
||||
// messagebuffer functions/objects.
|
||||
AsyncWebSocketMessageBuffer *makeBuffer(size_t size = 0);
|
||||
@@ -465,11 +473,16 @@ class AsyncWebSocketResponse : public AsyncWebServerResponse {
|
||||
private:
|
||||
String _content;
|
||||
AsyncWebSocket *_server;
|
||||
AsyncWebServerRequest *_request;
|
||||
// this call back will switch AsyncTCP client to WebSocket
|
||||
void _switchClient();
|
||||
|
||||
public:
|
||||
AsyncWebSocketResponse(const String &key, AsyncWebSocket *server);
|
||||
void _respond(AsyncWebServerRequest *request);
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
|
||||
return 0;
|
||||
};
|
||||
bool _sourceValid() const {
|
||||
return true;
|
||||
}
|
||||
@@ -556,5 +569,3 @@ private:
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
#endif /* ASYNCWEBSOCKET_H_ */
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#if ESP_IDF_VERSION_MAJOR < 5
|
||||
|
||||
#ifndef SHA1Builder_h
|
||||
#define SHA1Builder_h
|
||||
|
||||
#include <Stream.h>
|
||||
#include <WString.h>
|
||||
|
||||
@@ -39,6 +38,4 @@ public:
|
||||
void getBytes(uint8_t *output);
|
||||
};
|
||||
|
||||
#endif // SHA1Builder_h
|
||||
|
||||
#endif // ESP_IDF_VERSION_MAJOR < 5
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <ChunkPrint.h>
|
||||
|
||||
ChunkPrint::ChunkPrint(uint8_t *destination, size_t from, size_t len) : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {}
|
||||
|
||||
size_t ChunkPrint::write(uint8_t c) {
|
||||
if (_to_skip > 0) {
|
||||
_to_skip--;
|
||||
return 1;
|
||||
} else if (_to_write > 0) {
|
||||
_to_write--;
|
||||
_destination[_pos++] = c;
|
||||
// handle case where len is zero
|
||||
if (!_len) {
|
||||
return 0;
|
||||
}
|
||||
// skip first bytes until from is zero (bytes were already sent by previous chunk)
|
||||
if (_from) {
|
||||
_from--;
|
||||
return 1;
|
||||
}
|
||||
// write a maximum of len bytes
|
||||
if (_len - _index) {
|
||||
_destination[_index++] = c;
|
||||
return 1;
|
||||
}
|
||||
// we have finished writing len bytes, ignore the rest
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef CHUNKPRINT_H
|
||||
#define CHUNKPRINT_H
|
||||
#pragma once
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
class ChunkPrint : public Print {
|
||||
private:
|
||||
uint8_t *_destination;
|
||||
size_t _to_skip;
|
||||
size_t _to_write;
|
||||
size_t _pos;
|
||||
size_t _from;
|
||||
size_t _len;
|
||||
size_t _index;
|
||||
|
||||
public:
|
||||
ChunkPrint(uint8_t *destination, size_t from, size_t len);
|
||||
ChunkPrint(uint8_t *destination, size_t from, size_t len) : _destination(destination), _from(from), _len(len), _index(0) {}
|
||||
size_t write(uint8_t c);
|
||||
size_t write(const uint8_t *buffer, size_t size) {
|
||||
return this->Print::write(buffer, size);
|
||||
}
|
||||
size_t written() const {
|
||||
return _index;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef _ESPAsyncWebServer_H_
|
||||
#define _ESPAsyncWebServer_H_
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <FS.h>
|
||||
@@ -12,11 +11,32 @@
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR >= 5
|
||||
#define ASYNC_JSON_SUPPORT 1
|
||||
#else
|
||||
#define ASYNC_JSON_SUPPORT 0
|
||||
#endif // ARDUINOJSON_VERSION_MAJOR >= 5
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
#define ASYNC_MSG_PACK_SUPPORT 1
|
||||
#else
|
||||
#define ASYNC_MSG_PACK_SUPPORT 0
|
||||
#endif // ARDUINOJSON_VERSION_MAJOR >= 6
|
||||
|
||||
#endif // __has_include("ArduinoJson.h")
|
||||
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <assert.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
@@ -27,23 +47,27 @@
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
#include "literals.h"
|
||||
|
||||
#include "AsyncWebServerVersion.h"
|
||||
#define ASYNCWEBSERVER_FORK_ESP32Async
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE
|
||||
#else
|
||||
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined")))
|
||||
#include <regex>
|
||||
#endif
|
||||
|
||||
#include "./literals.h"
|
||||
|
||||
// See https://github.com/ESP32Async/ESPAsyncWebServer/commit/3d3456e9e81502a477f6498c44d0691499dda8f9#diff-646b25b11691c11dce25529e3abce843f0ba4bd07ab75ec9eee7e72b06dbf13fR388-R392
|
||||
// This setting slowdown chunk serving but avoids crashing or deadlocks in the case where slow chunk responses are created, like file serving form SD Card
|
||||
#ifndef ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
#define ASYNCWEBSERVER_USE_CHUNK_INFLIGHT 1
|
||||
#endif
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED || defined(ESP8266)
|
||||
#define ASYNCWEBSERVER_WIFI_SUPPORTED 1
|
||||
#else
|
||||
#define ASYNCWEBSERVER_WIFI_SUPPORTED 0
|
||||
#endif
|
||||
|
||||
class AsyncWebServer;
|
||||
class AsyncWebServerRequest;
|
||||
class AsyncWebServerResponse;
|
||||
@@ -204,6 +228,8 @@ class AsyncWebServerRequest {
|
||||
friend class AsyncWebServer;
|
||||
friend class AsyncCallbackWebHandler;
|
||||
friend class AsyncFileResponse;
|
||||
friend class AsyncStaticWebHandler;
|
||||
friend class AsyncURIMatcher;
|
||||
|
||||
private:
|
||||
AsyncClient *_client;
|
||||
@@ -236,7 +262,9 @@ private:
|
||||
|
||||
std::list<AsyncWebHeader> _headers;
|
||||
std::list<AsyncWebParameter> _params;
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
std::list<String> _pathParams;
|
||||
#endif
|
||||
|
||||
std::unordered_map<const char *, String, std::hash<const char *>, std::equal_to<const char *>> _attributes;
|
||||
|
||||
@@ -259,8 +287,6 @@ private:
|
||||
void _onDisconnect();
|
||||
void _onData(void *buf, size_t len);
|
||||
|
||||
void _addPathParam(const char *param);
|
||||
|
||||
bool _parseReqHead();
|
||||
bool _parseReqHeader();
|
||||
void _parseLine();
|
||||
@@ -275,7 +301,7 @@ private:
|
||||
void _send();
|
||||
void _runMiddlewareChain();
|
||||
|
||||
static void _getEtag(uint8_t trailer[4], char *serverETag);
|
||||
static bool _getEtag(File gzFile, char *eTag);
|
||||
|
||||
public:
|
||||
File _tempFile;
|
||||
@@ -287,6 +313,19 @@ public:
|
||||
AsyncClient *client() {
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief release owned AsyncClient object
|
||||
* AsyncClient pointer will be abandoned in this instance,
|
||||
* the further ownership of the connection should be managed out of request's life-time scope
|
||||
* could be used for long lived connection like SSE or WebSockets
|
||||
* @note do not call this method unless you know what you are doing, otherwise it may lead to
|
||||
* memory leaks and connections lingering
|
||||
*
|
||||
* @return AsyncClient* pointer to released connection object
|
||||
*/
|
||||
AsyncClient *clientRelease();
|
||||
|
||||
uint8_t version() const {
|
||||
return _version;
|
||||
}
|
||||
@@ -338,6 +377,17 @@ public:
|
||||
}
|
||||
void requestAuthentication(AsyncAuthType method, const char *realm = nullptr, const char *_authFailMsg = nullptr);
|
||||
|
||||
// detected Authentication type from "Authorization" request header during request parsing
|
||||
AsyncAuthType authType() const {
|
||||
return _authMethod;
|
||||
}
|
||||
|
||||
// raw value of "Authorization" request header after the auth type
|
||||
// For example, for header "Authorization: Bearer <token>", <token> is the value returned
|
||||
const String &authChallenge() const {
|
||||
return _authorization;
|
||||
}
|
||||
|
||||
// IMPORTANT: this method is for internal use ONLY
|
||||
// Please do not use it!
|
||||
// It can be removed or modified at any time without notice
|
||||
@@ -586,10 +636,22 @@ public:
|
||||
bool hasArg(const __FlashStringHelper *data) const; // check if F(argument) exists
|
||||
#endif
|
||||
|
||||
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
|
||||
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const {
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
const String &pathArg(size_t i) const {
|
||||
if (i >= _pathParams.size()) {
|
||||
return emptyString;
|
||||
}
|
||||
auto it = _pathParams.begin();
|
||||
std::advance(it, i);
|
||||
return *it;
|
||||
}
|
||||
const String &pathArg(int i) const {
|
||||
return i < 0 ? emptyString : pathArg((size_t)i);
|
||||
}
|
||||
#else
|
||||
const String &pathArg(size_t i) const __attribute__((error("ERR: pathArg() requires -D ASYNCWEBSERVER_REGEX and only works on regex handlers")));
|
||||
const String &pathArg(int i) const __attribute__((error("ERR: pathArg() requires -D ASYNCWEBSERVER_REGEX and only works on regex handlers")));
|
||||
#endif
|
||||
|
||||
// get request header value by name
|
||||
const String &header(const char *name) const;
|
||||
@@ -690,6 +752,235 @@ public:
|
||||
String urlDecode(const String &text) const;
|
||||
};
|
||||
|
||||
class AsyncURIMatcher {
|
||||
private:
|
||||
// Matcher types are internal, not part of public API
|
||||
enum class Type {
|
||||
None, // default state: matcher does not match anything
|
||||
All, // matches everything
|
||||
Exact, // matches equivalent to regex: ^{_uri}$
|
||||
Prefix, // matches equivalent to regex: ^{_uri}.*
|
||||
Extension, // non-regular match: /pattern../*.ext
|
||||
BackwardCompatible, // matches equivalent to regex: ^{_uri}(/.*)?$
|
||||
Regex, // matches _url as regex
|
||||
};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief No special matching behavior (default)
|
||||
*/
|
||||
static constexpr uint16_t None = 0;
|
||||
|
||||
/**
|
||||
* @brief Enable case-insensitive URI matching
|
||||
*
|
||||
* When CaseInsensitive is specified:
|
||||
* - The URI pattern is converted to lowercase during construction
|
||||
* - Incoming request URLs are converted to lowercase before matching
|
||||
* - For regex matchers, the std::regex::icase flag is used
|
||||
*
|
||||
* Example usage:
|
||||
* ```cpp
|
||||
* // Matches /login, /LOGIN, /Login, /LoGiN, etc.
|
||||
* server.on(AsyncURIMatcher::exact("/login", AsyncURIMatcher::CaseInsensitive), handler);
|
||||
*
|
||||
* // Matches /api/\*, /API/\*, /Api/\*, etc.
|
||||
* server.on(AsyncURIMatcher::prefix("/api", AsyncURIMatcher::CaseInsensitive), handler);
|
||||
*
|
||||
* // Regex with case insensitive matching
|
||||
* server.on(AsyncURIMatcher::regex("^/user/([a-z]+)$", AsyncURIMatcher::CaseInsensitive), handler);
|
||||
* ```
|
||||
*
|
||||
* Performance note: Case conversion adds minimal overhead during construction and matching.
|
||||
*/
|
||||
static constexpr uint16_t CaseInsensitive = (1 << 0);
|
||||
|
||||
// public constructors
|
||||
AsyncURIMatcher() : AsyncURIMatcher({}, Type::None, None) {}
|
||||
AsyncURIMatcher(const char *uri, uint16_t modifiers = None) : AsyncURIMatcher(String(uri), modifiers) {}
|
||||
AsyncURIMatcher(String uri, uint16_t modifiers = None);
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
AsyncURIMatcher(const AsyncURIMatcher &c);
|
||||
AsyncURIMatcher(AsyncURIMatcher &&c);
|
||||
~AsyncURIMatcher();
|
||||
|
||||
AsyncURIMatcher &operator=(const AsyncURIMatcher &r);
|
||||
AsyncURIMatcher &operator=(AsyncURIMatcher &&r);
|
||||
|
||||
#else
|
||||
AsyncURIMatcher(const AsyncURIMatcher &) = default;
|
||||
AsyncURIMatcher(AsyncURIMatcher &&) = default;
|
||||
~AsyncURIMatcher() = default;
|
||||
|
||||
AsyncURIMatcher &operator=(const AsyncURIMatcher &) = default;
|
||||
AsyncURIMatcher &operator=(AsyncURIMatcher &&) = default;
|
||||
#endif
|
||||
|
||||
bool matches(AsyncWebServerRequest *request) const;
|
||||
|
||||
// static factory methods for common match types:
|
||||
// - AsyncURIMatcher::all() - matches everything
|
||||
// - AsyncURIMatcher::none() - matches nothing
|
||||
// - AsyncURIMatcher::exact(uri, modifiers) - exact match
|
||||
// - AsyncURIMatcher::prefix(uri, modifiers) - prefix match
|
||||
// - AsyncURIMatcher::dir(uri, modifiers) - directory/folder match (trailing slash added automatically)
|
||||
// - AsyncURIMatcher::ext(uri, modifiers) - extension match (pattern with wildcard)
|
||||
// - AsyncURIMatcher::regex(uri, modifiers) - regex match (requires ASYNCWEBSERVER_REGEX)
|
||||
|
||||
/**
|
||||
* @brief Create a matcher that matches all URIs unconditionally
|
||||
* @return AsyncURIMatcher that accepts any request URL
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::all(), handler);
|
||||
*/
|
||||
static inline AsyncURIMatcher all() {
|
||||
return AsyncURIMatcher{{}, Type::All, None};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a matcher that matches no URIs (never matches)
|
||||
* @return AsyncURIMatcher that rejects all request URLs
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::none(), handler);
|
||||
*/
|
||||
static inline AsyncURIMatcher none() {
|
||||
return AsyncURIMatcher{{}, Type::None, None};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create an exact URI matcher
|
||||
* @param c The exact URI string to match (e.g., "/login", "/api/status")
|
||||
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
|
||||
* @return AsyncURIMatcher that matches only the exact URI
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::exact("/login"), handler);
|
||||
* Matches: "/login"
|
||||
* Doesn't match: "/login/", "/login-page"
|
||||
* Doesn't match: "/LOGIN" (unless CaseInsensitive flag used)
|
||||
*/
|
||||
static inline AsyncURIMatcher exact(String c, uint16_t modifiers = None) {
|
||||
return AsyncURIMatcher{std::move(c), Type::Exact, modifiers};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a prefix URI matcher
|
||||
* @param c The URI prefix to match (e.g., "/api", "/static")
|
||||
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
|
||||
* @return AsyncURIMatcher that matches URIs starting with the prefix
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::prefix("/api"), handler);
|
||||
* Matches: "/api", "/api/users", "/api-v2", "/apitest"
|
||||
* Note: This is pure prefix matching - does NOT require folder separator
|
||||
*/
|
||||
static inline AsyncURIMatcher prefix(String c, uint16_t modifiers = None) {
|
||||
return AsyncURIMatcher{std::move(c), Type::Prefix, modifiers};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a directory/folder URI matcher
|
||||
* @param c The directory path (trailing slash automatically added if missing)
|
||||
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
|
||||
* @return AsyncURIMatcher that matches URIs under the directory
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::dir("/admin"), handler);
|
||||
* Matches: "/admin/users", "/admin/settings", "/admin/sub/path"
|
||||
* Doesn't match: "/admin" (exact), "/admin-panel" (no folder separator)
|
||||
*
|
||||
* The trailing slash is automatically added for convenience and efficiency.
|
||||
*/
|
||||
static inline AsyncURIMatcher dir(String c, uint16_t modifiers = None) {
|
||||
// Pre-calculate folder for efficiency
|
||||
if (!c.length()) {
|
||||
return AsyncURIMatcher{"/", Type::Prefix, modifiers};
|
||||
}
|
||||
if (c[c.length() - 1] != '/') {
|
||||
c.concat('/');
|
||||
}
|
||||
return AsyncURIMatcher{std::move(c), Type::Prefix, modifiers};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a file extension URI matcher
|
||||
* @param c The pattern with wildcard extension (e.g., "/images/\*.jpg", "/docs/\*.pdf")
|
||||
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
|
||||
* @return AsyncURIMatcher that matches files with specific extensions under a path
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::ext("/images/\*.jpg"), handler);
|
||||
* Matches: "/images/photo.jpg", "/images/gallery/pic.jpg"
|
||||
* Doesn't match: "/images/photo.png", "/img/photo.jpg"
|
||||
*
|
||||
* Pattern format: "/path/\*.extension" where "*" is a literal wildcard placeholder.
|
||||
* The path before "/\*." must match exactly, and the URI must end with the extension.
|
||||
*/
|
||||
static inline AsyncURIMatcher ext(String c, uint16_t modifiers = None) {
|
||||
return AsyncURIMatcher{std::move(c), Type::Extension, modifiers};
|
||||
}
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
/**
|
||||
* @brief Create a regular expression URI matcher
|
||||
* @param c The regex pattern string (e.g., "^/user/([0-9]+)$", "^/blog/([0-9]{4})/([0-9]{2})$")
|
||||
* @param modifiers Optional modifiers (CaseInsensitive applies to regex compilation)
|
||||
* @return AsyncURIMatcher that matches URIs using regex with capture groups
|
||||
*
|
||||
* Usage: server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
|
||||
* Matches: "/user/123", "/user/456"
|
||||
* Doesn't match: "/user/abc", "/user/123/profile"
|
||||
*
|
||||
* Captured groups can be accessed via request->pathArg(index) in the handler.
|
||||
* Requires ASYNCWEBSERVER_REGEX to be defined during compilation.
|
||||
* Performance note: Regex matching is slower than other match types.
|
||||
*/
|
||||
static inline AsyncURIMatcher regex(String c, uint16_t modifiers = None) {
|
||||
return AsyncURIMatcher{std::move(c), Type::Regex, modifiers};
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
// fields
|
||||
String _value;
|
||||
union {
|
||||
intptr_t _flags; // type and flags packed together
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
// Overlay the pattern pointer storage with the type. It is treated as a tagged pointer:
|
||||
// if any of the LSBs are set, it stores type, as a valid object must be aligned and so
|
||||
// none of the LSBs can be set in a valid pointer.
|
||||
std::regex *pattern;
|
||||
#endif
|
||||
};
|
||||
|
||||
// private constructor called from static factory methods
|
||||
AsyncURIMatcher(String uri, Type type, uint16_t modifiers);
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
inline bool _isRegex() const {
|
||||
static_assert(
|
||||
(std::alignment_of<std::regex>::value % 2) == 0, "Unexpected regex type alignment - please let the ESPAsyncWebServer team know about your platform!"
|
||||
);
|
||||
// pattern is non-null pointer with correct alignment.
|
||||
// We use the _flags view as it's already a integer type.
|
||||
return _flags && !(_flags & (std::alignment_of<std::regex>::value - 1));
|
||||
}
|
||||
#endif
|
||||
|
||||
static constexpr intptr_t _toFlags(Type type, uint16_t modifiers) {
|
||||
// Use lsb to disambiguate from regex pointer in the case where someone has regex activated but uses a non-regex type.
|
||||
// We always do this shift, even if regex is not enabled, to keep the layout identical and also catch programmatic errors earlier.
|
||||
// For example a mistake is to set a modifier flag to (1 << 15), which is the msb of the uint16_t.
|
||||
// This msb is discarded during this shift operation.
|
||||
// So pay attention to not have more than 15 modifier flags.
|
||||
return ((uint32_t(modifiers) << 16 | uint16_t(type)) << 1) + 1;
|
||||
}
|
||||
|
||||
static constexpr std::tuple<Type, uint16_t> _fromFlags(intptr_t in_flags) {
|
||||
// shift off disambiguation bit
|
||||
// - Type is lower 16 bits
|
||||
// - Modifiers are upper 16 bits
|
||||
return std::make_tuple(static_cast<Type>((in_flags >> 1) & 0xFFFF), (in_flags >> 1) >> 16);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server)
|
||||
* */
|
||||
@@ -756,10 +1047,35 @@ protected:
|
||||
// AsyncAuthenticationMiddleware is a middleware that checks if the request is authenticated
|
||||
class AsyncAuthenticationMiddleware : public AsyncMiddleware {
|
||||
public:
|
||||
const String &username() const {
|
||||
return _username;
|
||||
}
|
||||
const String &credentials() const {
|
||||
return _credentials;
|
||||
}
|
||||
const String &realm() const {
|
||||
return _realm;
|
||||
}
|
||||
const String &authFailureMessage() const {
|
||||
return _authFailMsg;
|
||||
}
|
||||
bool isHash() const {
|
||||
return _hash;
|
||||
}
|
||||
AsyncAuthType authType() const {
|
||||
return _authMethod;
|
||||
}
|
||||
|
||||
void setUsername(const char *username);
|
||||
void setPassword(const char *password);
|
||||
void setPasswordHash(const char *hash);
|
||||
|
||||
// can be used for Bearer token authentication with a static shared secret
|
||||
void setToken(const char *token);
|
||||
void setAuthentificationFunction(std::function<bool(AsyncWebServerRequest *request)> func) {
|
||||
_authcFunc = func;
|
||||
}
|
||||
|
||||
void setRealm(const char *realm) {
|
||||
_realm = realm;
|
||||
}
|
||||
@@ -802,6 +1118,9 @@ private:
|
||||
AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE;
|
||||
String _authFailMsg;
|
||||
bool _hasCreds = false;
|
||||
std::function<bool(AsyncWebServerRequest *request)> _authcFunc = [this](AsyncWebServerRequest *request) {
|
||||
return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash);
|
||||
};
|
||||
};
|
||||
|
||||
using ArAuthorizeFunction = std::function<bool(AsyncWebServerRequest *request)>;
|
||||
@@ -891,7 +1210,13 @@ public:
|
||||
_maxAge = seconds;
|
||||
}
|
||||
|
||||
void addCORSHeaders(AsyncWebServerResponse *response);
|
||||
#ifndef ESP8266
|
||||
[[deprecated("Use instead: addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response)")]]
|
||||
#endif
|
||||
void addCORSHeaders(AsyncWebServerResponse *response) {
|
||||
addCORSHeaders(nullptr, response);
|
||||
}
|
||||
void addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response);
|
||||
|
||||
void run(AsyncWebServerRequest *request, ArMiddlewareNext next);
|
||||
|
||||
@@ -1032,15 +1357,18 @@ protected:
|
||||
bool _sendContentLength;
|
||||
bool _chunked;
|
||||
size_t _headLength;
|
||||
// amount of data sent for content part of the response (excluding all headers)
|
||||
size_t _sentLength;
|
||||
size_t _ackedLength;
|
||||
// amount of response bytes (including all headers) written to sockbuff for delivery
|
||||
size_t _writtenLength;
|
||||
WebResponseState _state;
|
||||
|
||||
static bool headerMustBePresentOnce(const String &name);
|
||||
|
||||
public:
|
||||
static const char *responseCodeToString(int code);
|
||||
// Return type changes based on platform (const char* or __FlashStringHelper*)
|
||||
static STR_RETURN_TYPE responseCodeToString(int code);
|
||||
|
||||
public:
|
||||
AsyncWebServerResponse();
|
||||
@@ -1090,7 +1418,20 @@ public:
|
||||
virtual bool _failed() const;
|
||||
virtual bool _sourceValid() const;
|
||||
virtual void _respond(AsyncWebServerRequest *request);
|
||||
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
|
||||
|
||||
/**
|
||||
* @brief write next portion of response data to send buffs
|
||||
* this method (re)fills tcp send buffers, it could be called either at will
|
||||
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
|
||||
*
|
||||
* @param request - used to access client object
|
||||
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
|
||||
* @param time - time passed between last sent and received packet
|
||||
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
|
||||
*/
|
||||
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
return 0;
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -1102,6 +1443,20 @@ typedef std::function<void(AsyncWebServerRequest *request, const String &filenam
|
||||
ArUploadHandlerFunction;
|
||||
typedef std::function<void(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)> ArBodyHandlerFunction;
|
||||
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
|
||||
class AsyncCallbackJsonWebHandler;
|
||||
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction;
|
||||
|
||||
#if ASYNC_MSG_PACK_SUPPORT == 1
|
||||
#ifndef ESP8266
|
||||
[[deprecated("Replaced by AsyncCallbackJsonWebHandler")]]
|
||||
#endif
|
||||
typedef AsyncCallbackJsonWebHandler AsyncCallbackMessagePackWebHandler;
|
||||
#endif // ASYNC_MSG_PACK_SUPPORT
|
||||
|
||||
#endif
|
||||
|
||||
class AsyncWebServer : public AsyncMiddlewareChain {
|
||||
protected:
|
||||
AsyncServer _server;
|
||||
@@ -1174,14 +1529,18 @@ public:
|
||||
AsyncWebHandler &addHandler(AsyncWebHandler *handler);
|
||||
bool removeHandler(AsyncWebHandler *handler);
|
||||
|
||||
AsyncCallbackWebHandler &on(const char *uri, ArRequestHandlerFunction onRequest) {
|
||||
return on(uri, HTTP_ANY, onRequest);
|
||||
AsyncCallbackWebHandler &on(AsyncURIMatcher uri, ArRequestHandlerFunction onRequest) {
|
||||
return on(std::move(uri), HTTP_ANY, onRequest);
|
||||
}
|
||||
AsyncCallbackWebHandler &on(
|
||||
const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr,
|
||||
AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr,
|
||||
ArBodyHandlerFunction onBody = nullptr
|
||||
);
|
||||
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
AsyncCallbackJsonWebHandler &on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody);
|
||||
#endif
|
||||
|
||||
AsyncStaticWebHandler &serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control = NULL);
|
||||
|
||||
void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned
|
||||
@@ -1231,4 +1590,6 @@ public:
|
||||
#include "WebHandlerImpl.h"
|
||||
#include "WebResponseImpl.h"
|
||||
|
||||
#endif /* _AsyncWebServer_H_ */
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
#include <AsyncJson.h>
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "WebAuthentication.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include <list>
|
||||
|
||||
AsyncMiddlewareChain::~AsyncMiddlewareChain() {
|
||||
for (AsyncMiddleware *m : _middlewares) {
|
||||
if (m->_freeOnRemoval) {
|
||||
@@ -85,6 +87,12 @@ void AsyncAuthenticationMiddleware::setPasswordHash(const char *hash) {
|
||||
_hasCreds = _username.length() && _credentials.length();
|
||||
}
|
||||
|
||||
void AsyncAuthenticationMiddleware::setToken(const char *token) {
|
||||
_credentials = token;
|
||||
_hash = _credentials.length();
|
||||
_hasCreds = _credentials.length();
|
||||
}
|
||||
|
||||
bool AsyncAuthenticationMiddleware::generateHash() {
|
||||
// ensure we have all the necessary data
|
||||
if (!_hasCreds) {
|
||||
@@ -120,19 +128,15 @@ bool AsyncAuthenticationMiddleware::generateHash() {
|
||||
}
|
||||
|
||||
bool AsyncAuthenticationMiddleware::allowed(AsyncWebServerRequest *request) const {
|
||||
if (_authMethod == AsyncAuthType::AUTH_NONE) {
|
||||
return true;
|
||||
switch (_authMethod) {
|
||||
case AsyncAuthType::AUTH_NONE: return true;
|
||||
case AsyncAuthType::AUTH_DENIED: return false;
|
||||
case AsyncAuthType::AUTH_BEARER: return _authcFunc(request);
|
||||
case AsyncAuthType::AUTH_OTHER: return _authcFunc(request);
|
||||
case AsyncAuthType::AUTH_BASIC: return !_hasCreds || _authcFunc(request);
|
||||
case AsyncAuthType::AUTH_DIGEST: return !_hasCreds || _authcFunc(request);
|
||||
default: return false;
|
||||
}
|
||||
|
||||
if (_authMethod == AsyncAuthType::AUTH_DENIED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_hasCreds) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash);
|
||||
}
|
||||
|
||||
void AsyncAuthenticationMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) {
|
||||
@@ -228,8 +232,13 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerResponse *response) {
|
||||
response->addHeader(asyncsrv::T_CORS_ACAO, _origin.c_str());
|
||||
void AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response) {
|
||||
if (request != nullptr && _credentials && _origin == "*") {
|
||||
// cannot use wildcard when allowing credentials
|
||||
response->addHeader(asyncsrv::T_CORS_ACAO, request->header(asyncsrv::T_CORS_O).c_str());
|
||||
} else {
|
||||
response->addHeader(asyncsrv::T_CORS_ACAO, _origin.c_str());
|
||||
}
|
||||
response->addHeader(asyncsrv::T_CORS_ACAM, _methods.c_str());
|
||||
response->addHeader(asyncsrv::T_CORS_ACAH, _headers.c_str());
|
||||
response->addHeader(asyncsrv::T_CORS_ACAC, _credentials ? asyncsrv::T_TRUE : asyncsrv::T_FALSE);
|
||||
@@ -242,7 +251,7 @@ void AsyncCorsMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext n
|
||||
// check if this is a preflight request => handle it and return
|
||||
if (request->method() == HTTP_OPTIONS) {
|
||||
AsyncWebServerResponse *response = request->beginResponse(200);
|
||||
addCORSHeaders(response);
|
||||
addCORSHeaders(request, response);
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
@@ -251,7 +260,7 @@ void AsyncCorsMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext n
|
||||
next();
|
||||
AsyncWebServerResponse *response = request->getResponse();
|
||||
if (response) {
|
||||
addCORSHeaders(response);
|
||||
addCORSHeaders(request, response);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "WebAuthentication.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <libb64/cencode.h>
|
||||
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <MD5Builder.h>
|
||||
#else
|
||||
#include "md5.h"
|
||||
#include <md5.h>
|
||||
#endif
|
||||
#include "literals.h"
|
||||
|
||||
#include "./literals.h"
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
@@ -82,13 +85,11 @@ String genRandomMD5() {
|
||||
#ifdef ESP8266
|
||||
uint32_t r = RANDOM_REG32;
|
||||
#else
|
||||
uint32_t r = rand();
|
||||
uint32_t r = rand(); // NOLINT(runtime/threadsafe_fn)
|
||||
#endif
|
||||
char *out = (char *)malloc(33);
|
||||
if (out == NULL || !getMD5((uint8_t *)(&r), 4, out)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
String res = String(out);
|
||||
@@ -99,9 +100,7 @@ String genRandomMD5() {
|
||||
static String stringMD5(const String &in) {
|
||||
char *out = (char *)malloc(33);
|
||||
if (out == NULL || !getMD5((uint8_t *)(in.c_str()), in.length(), out)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
String res = String(out);
|
||||
@@ -115,17 +114,13 @@ String generateDigestHash(const char *username, const char *password, const char
|
||||
}
|
||||
char *out = (char *)malloc(33);
|
||||
if (out == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
String in;
|
||||
if (!in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
free(out);
|
||||
return emptyString;
|
||||
}
|
||||
@@ -137,9 +132,7 @@ String generateDigestHash(const char *username, const char *password, const char
|
||||
in.concat(password);
|
||||
|
||||
if (!getMD5((uint8_t *)(in.c_str()), in.length(), out)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
free(out);
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef WEB_AUTHENTICATION_H_
|
||||
#define WEB_AUTHENTICATION_H_
|
||||
#pragma once
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
@@ -19,5 +18,3 @@ String generateDigestHash(const char *username, const char *password, const char
|
||||
String generateBasicHash(const char *username, const char *password);
|
||||
|
||||
String genRandomMD5();
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef ASYNCWEBSERVERHANDLERIMPL_H_
|
||||
#define ASYNCWEBSERVERHANDLERIMPL_H_
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <string>
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
#include <regex>
|
||||
#endif
|
||||
|
||||
#include "stddef.h"
|
||||
#include <time.h>
|
||||
|
||||
class AsyncStaticWebHandler : public AsyncWebHandler {
|
||||
using File = fs::File;
|
||||
@@ -33,8 +29,8 @@ protected:
|
||||
|
||||
public:
|
||||
AsyncStaticWebHandler(const char *uri, FS &fs, const char *path, const char *cache_control);
|
||||
bool canHandle(AsyncWebServerRequest *request) const override final;
|
||||
void handleRequest(AsyncWebServerRequest *request) override final;
|
||||
bool canHandle(AsyncWebServerRequest *request) const final;
|
||||
void handleRequest(AsyncWebServerRequest *request) final;
|
||||
AsyncStaticWebHandler &setTryGzipFirst(bool value);
|
||||
AsyncStaticWebHandler &setIsDir(bool isDir);
|
||||
AsyncStaticWebHandler &setDefaultFile(const char *filename);
|
||||
@@ -58,7 +54,7 @@ public:
|
||||
class AsyncCallbackWebHandler : public AsyncWebHandler {
|
||||
private:
|
||||
protected:
|
||||
String _uri;
|
||||
AsyncURIMatcher _uri;
|
||||
WebRequestMethodComposite _method;
|
||||
ArRequestHandlerFunction _onRequest;
|
||||
ArUploadHandlerFunction _onUpload;
|
||||
@@ -67,7 +63,7 @@ protected:
|
||||
|
||||
public:
|
||||
AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {}
|
||||
void setUri(const String &uri);
|
||||
void setUri(AsyncURIMatcher uri);
|
||||
void setMethod(WebRequestMethodComposite method) {
|
||||
_method = method;
|
||||
}
|
||||
@@ -81,13 +77,11 @@ public:
|
||||
_onBody = fn;
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) const override final;
|
||||
void handleRequest(AsyncWebServerRequest *request) override final;
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override final;
|
||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
|
||||
bool isRequestHandlerTrivial() const override final {
|
||||
bool canHandle(AsyncWebServerRequest *request) const final;
|
||||
void handleRequest(AsyncWebServerRequest *request) final;
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) final;
|
||||
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) final;
|
||||
bool isRequestHandlerTrivial() const final {
|
||||
return !_onRequest;
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebHandlerImpl.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
@@ -75,7 +79,7 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(struct tm *last_mo
|
||||
char result[30];
|
||||
#ifdef ESP8266
|
||||
auto formatP = PSTR("%a, %d %b %Y %H:%M:%S GMT");
|
||||
char format[strlen_P(formatP) + 1];
|
||||
char format[strlen_P(formatP) + 1]; // NOLINT(runtime/arrays)
|
||||
strcpy_P(format, formatP);
|
||||
#else
|
||||
static constexpr const char *format = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
@@ -173,9 +177,7 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
|
||||
size_t pathLen = path.length();
|
||||
char *_tempPath = (char *)malloc(pathLen + 1);
|
||||
if (_tempPath == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
request->_tempFile.close();
|
||||
return false;
|
||||
@@ -187,54 +189,74 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles an incoming HTTP request for a static file.
|
||||
*
|
||||
* This method processes a request for serving static files asynchronously.
|
||||
* It determines the correct ETag (entity tag) for caching, checks if the file
|
||||
* has been modified, and prepares the appropriate response (file response or 304 Not Modified).
|
||||
*
|
||||
* @param request Pointer to the incoming AsyncWebServerRequest object.
|
||||
*/
|
||||
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
// Get the filename from request->_tempObject and free it
|
||||
String filename((char *)request->_tempObject);
|
||||
free(request->_tempObject);
|
||||
request->_tempObject = NULL;
|
||||
request->_tempObject = nullptr;
|
||||
|
||||
if (request->_tempFile != true) {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS)
|
||||
// set etag to lastmod timestamp if available, otherwise to size
|
||||
String etag;
|
||||
if (lw) {
|
||||
setLastModified(lw);
|
||||
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
// time_t == long long int
|
||||
constexpr size_t len = 1 + 8 * sizeof(time_t);
|
||||
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
|
||||
// Get server ETag. If file is not GZ and we have a Template Processor, ETag is set to an empty string
|
||||
char etag[11];
|
||||
const char *tempFileName = request->_tempFile.name();
|
||||
const size_t lenFilename = strlen(tempFileName);
|
||||
|
||||
if (lenFilename > T__GZ_LEN && memcmp(tempFileName + lenFilename - T__GZ_LEN, T__gz, T__GZ_LEN) == 0) {
|
||||
//File is a gz, get etag from CRC in trailer
|
||||
if (!AsyncWebServerRequest::_getEtag(request->_tempFile, etag)) {
|
||||
// File is corrupted or invalid
|
||||
async_ws_log_e("File is corrupted or invalid: %s", tempFileName);
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset file position to the beginning so the file can be served from the start.
|
||||
request->_tempFile.seek(0);
|
||||
} else if (_callback == nullptr) {
|
||||
// We don't have a Template processor
|
||||
uint32_t etagValue;
|
||||
time_t lastWrite = request->_tempFile.getLastWrite();
|
||||
if (lastWrite > 0) {
|
||||
// Use timestamp-based ETag
|
||||
etagValue = static_cast<uint32_t>(lastWrite);
|
||||
} else {
|
||||
// No timestamp available, use filesize-based ETag
|
||||
size_t fileSize = request->_tempFile.size();
|
||||
etagValue = static_cast<uint32_t>(fileSize);
|
||||
}
|
||||
// RFC9110 Section-8.8.3: Value of the ETag response must be enclosed in double quotes
|
||||
snprintf(etag, sizeof(etag), "\"%08" PRIx32 "\"", etagValue);
|
||||
} else {
|
||||
#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();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool not_modified = false;
|
||||
|
||||
// if-none-match has precedence over if-modified-since
|
||||
if (request->hasHeader(T_INM)) {
|
||||
not_modified = request->header(T_INM).equals(etag);
|
||||
} else if (_last_modified.length()) {
|
||||
not_modified = request->header(T_IMS).equals(_last_modified);
|
||||
etag[0] = '\0';
|
||||
}
|
||||
|
||||
AsyncWebServerResponse *response;
|
||||
|
||||
if (not_modified) {
|
||||
bool notModified = false;
|
||||
// 1. If the client sent If-None-Match and we have an ETag → compare
|
||||
if (*etag != '\0' && request->header(T_INM) == etag) {
|
||||
notModified = true;
|
||||
}
|
||||
// 2. Otherwise, if there is no ETag but we have Last-Modified and Last-Modified matches
|
||||
else if (*etag == '\0' && _last_modified.length() > 0 && request->header(T_IMS) == _last_modified) {
|
||||
async_ws_log_d("_last_modified: %s", _last_modified.c_str());
|
||||
notModified = true;
|
||||
}
|
||||
|
||||
if (notModified) {
|
||||
request->_tempFile.close();
|
||||
response = new AsyncBasicResponse(304); // Not modified
|
||||
} else {
|
||||
@@ -242,20 +264,27 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
|
||||
response->addHeader(T_ETag, etag.c_str());
|
||||
|
||||
if (_last_modified.length()) {
|
||||
response->addHeader(T_Last_Modified, _last_modified.c_str());
|
||||
if (!notModified) {
|
||||
// Set ETag header
|
||||
if (*etag != '\0') {
|
||||
response->addHeader(T_ETag, etag, true);
|
||||
}
|
||||
// Set Last-Modified header
|
||||
if (_last_modified.length()) {
|
||||
response->addHeader(T_Last_Modified, _last_modified.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Set cache control
|
||||
if (_cache_control.length()) {
|
||||
response->addHeader(T_Cache_Control, _cache_control.c_str());
|
||||
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
|
||||
} else {
|
||||
response->addHeader(T_Cache_Control, T_no_cache, false);
|
||||
}
|
||||
|
||||
request->send(response);
|
||||
@@ -266,47 +295,15 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setTemplateProcessor(AwsTemplatePr
|
||||
return *this;
|
||||
}
|
||||
|
||||
void AsyncCallbackWebHandler::setUri(const String &uri) {
|
||||
_uri = uri;
|
||||
_isRegex = uri.startsWith("^") && uri.endsWith("$");
|
||||
void AsyncCallbackWebHandler::setUri(AsyncURIMatcher uri) {
|
||||
_uri = std::move(uri);
|
||||
}
|
||||
|
||||
bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest *request) const {
|
||||
if (!_onRequest || !request->isHTTP() || !(_method & request->method())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
if (_isRegex) {
|
||||
std::regex pattern(_uri.c_str());
|
||||
std::smatch matches;
|
||||
std::string s(request->url().c_str());
|
||||
if (std::regex_search(s, matches, pattern)) {
|
||||
for (size_t i = 1; i < matches.size(); ++i) { // start from 1
|
||||
request->_addPathParam(matches[i].str().c_str());
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
if (_uri.length() && _uri.startsWith("/*.")) {
|
||||
String uriTemplate = String(_uri);
|
||||
uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf("."));
|
||||
if (!request->url().endsWith(uriTemplate)) {
|
||||
return false;
|
||||
}
|
||||
} else if (_uri.length() && _uri.endsWith("*")) {
|
||||
String uriTemplate = String(_uri);
|
||||
uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1);
|
||||
if (!request->url().startsWith(uriTemplate)) {
|
||||
return false;
|
||||
}
|
||||
} else if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return _uri.matches(request);
|
||||
}
|
||||
|
||||
void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebAuthentication.h"
|
||||
#include "WebResponseImpl.h"
|
||||
#include "literals.h"
|
||||
#include <cstring>
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '='))
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "./literals.h"
|
||||
|
||||
static inline bool isParamChar(char c) {
|
||||
return ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '='));
|
||||
}
|
||||
|
||||
static void doNotDelete(AsyncWebServerRequest *) {}
|
||||
|
||||
@@ -29,72 +37,68 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
|
||||
c->onError(
|
||||
[](void *r, AsyncClient *c, int8_t error) {
|
||||
(void)c;
|
||||
// log_e("AsyncWebServerRequest::_onError");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onError(error);
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onError");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onError(error);
|
||||
},
|
||||
this
|
||||
);
|
||||
c->onAck(
|
||||
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
|
||||
(void)c;
|
||||
// log_e("AsyncWebServerRequest::_onAck");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onAck(len, time);
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onAck");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onAck(len, time);
|
||||
},
|
||||
this
|
||||
);
|
||||
c->onDisconnect(
|
||||
[](void *r, AsyncClient *c) {
|
||||
// log_e("AsyncWebServerRequest::_onDisconnect");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onDisconnect();
|
||||
delete c;
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onDisconnect");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onDisconnect();
|
||||
},
|
||||
this
|
||||
);
|
||||
c->onTimeout(
|
||||
[](void *r, AsyncClient *c, uint32_t time) {
|
||||
(void)c;
|
||||
// log_e("AsyncWebServerRequest::_onTimeout");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onTimeout(time);
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onTimeout");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onTimeout(time);
|
||||
},
|
||||
this
|
||||
);
|
||||
c->onData(
|
||||
[](void *r, AsyncClient *c, void *buf, size_t len) {
|
||||
(void)c;
|
||||
// log_e("AsyncWebServerRequest::_onData");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onData(buf, len);
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onData");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onData(buf, len);
|
||||
},
|
||||
this
|
||||
);
|
||||
c->onPoll(
|
||||
[](void *r, AsyncClient *c) {
|
||||
(void)c;
|
||||
// log_e("AsyncWebServerRequest::_onPoll");
|
||||
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
|
||||
req->_onPoll();
|
||||
// async_ws_log_e("AsyncWebServerRequest::_onPoll");
|
||||
static_cast<AsyncWebServerRequest *>(r)->_onPoll();
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
AsyncWebServerRequest::~AsyncWebServerRequest() {
|
||||
// log_e("AsyncWebServerRequest::~AsyncWebServerRequest");
|
||||
if (_client) {
|
||||
// usually it is _client's disconnect triggers object destruct, but for completeness we define behavior
|
||||
// if for some reason *this will be destructed while client is still connected
|
||||
_client->onDisconnect(nullptr);
|
||||
delete _client;
|
||||
_client = nullptr;
|
||||
}
|
||||
|
||||
if (_response) {
|
||||
delete _response;
|
||||
_response = nullptr;
|
||||
}
|
||||
|
||||
_this.reset();
|
||||
|
||||
_headers.clear();
|
||||
|
||||
_pathParams.clear();
|
||||
|
||||
AsyncWebServerResponse *r = _response;
|
||||
_response = NULL;
|
||||
delete r;
|
||||
|
||||
if (_tempObject != NULL) {
|
||||
free(_tempObject);
|
||||
}
|
||||
@@ -112,9 +116,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
|
||||
// SSL/TLS handshake detection
|
||||
#ifndef ASYNC_TCP_SSL_ENABLED
|
||||
if (_parseState == PARSE_REQ_START && len && ((uint8_t *)buf)[0] == 0x16) { // 0x16 indicates a Handshake message (SSL/TLS).
|
||||
#ifdef ESP32
|
||||
log_d("SSL/TLS handshake detected: resetting connection");
|
||||
#endif
|
||||
async_ws_log_d("SSL/TLS handshake detected: resetting connection");
|
||||
_parseState = PARSE_REQ_FAIL;
|
||||
abort();
|
||||
return;
|
||||
@@ -142,9 +144,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
|
||||
char ch = str[len - 1];
|
||||
str[len - 1] = 0;
|
||||
if (!_temp.reserve(_temp.length() + len)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
_parseState = PARSE_REQ_FAIL;
|
||||
abort();
|
||||
return;
|
||||
@@ -183,9 +183,12 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
|
||||
if (_parsedLength == 0) {
|
||||
if (_contentType.startsWith(T_app_xform_urlencoded)) {
|
||||
_isPlainPost = true;
|
||||
} else if (_contentType == T_text_plain && __is_param_char(((char *)buf)[0])) {
|
||||
} else if (_contentType == T_text_plain && isParamChar(((char *)buf)[0])) {
|
||||
size_t i = 0;
|
||||
while (i < len && __is_param_char(((char *)buf)[i++]));
|
||||
char ch;
|
||||
do {
|
||||
ch = ((char *)buf)[i];
|
||||
} while (i++ < len && isParamChar(ch));
|
||||
if (i < len && ((char *)buf)[i - 1] == '=') {
|
||||
_isPlainPost = true;
|
||||
}
|
||||
@@ -219,31 +222,26 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
|
||||
|
||||
void AsyncWebServerRequest::_onPoll() {
|
||||
// os_printf("p\n");
|
||||
if (_response != NULL && _client != NULL && _client->canSend()) {
|
||||
if (!_response->_finished()) {
|
||||
_response->_ack(this, 0, 0);
|
||||
} else {
|
||||
AsyncWebServerResponse *r = _response;
|
||||
_response = NULL;
|
||||
delete r;
|
||||
|
||||
_client->close();
|
||||
}
|
||||
if (_response && _client && _client->canSend()) {
|
||||
_response->_ack(this, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWebServerRequest::_onAck(size_t len, uint32_t time) {
|
||||
// os_printf("a:%u:%u\n", len, time);
|
||||
if (_response != NULL) {
|
||||
if (!_response->_finished()) {
|
||||
_response->_ack(this, len, time);
|
||||
} else if (_response->_finished()) {
|
||||
AsyncWebServerResponse *r = _response;
|
||||
_response = NULL;
|
||||
delete r;
|
||||
if (!_response) {
|
||||
return;
|
||||
}
|
||||
|
||||
_client->close();
|
||||
if (!_response->_finished()) {
|
||||
_response->_ack(this, len, time);
|
||||
// recheck if response has just completed, close connection
|
||||
if (_response->_finished()) {
|
||||
_client->close(); // this will trigger _onDisconnect() and object destruction
|
||||
}
|
||||
} else {
|
||||
// this will close responses that were complete via a single _send() call
|
||||
_client->close(); // this will trigger _onDisconnect() and object destruction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,10 +267,6 @@ void AsyncWebServerRequest::_onDisconnect() {
|
||||
_server->_handleDisconnect(this);
|
||||
}
|
||||
|
||||
void AsyncWebServerRequest::_addPathParam(const char *p) {
|
||||
_pathParams.emplace_back(p);
|
||||
}
|
||||
|
||||
void AsyncWebServerRequest::_addGetParams(const String ¶ms) {
|
||||
size_t start = 0;
|
||||
while (start < params.length()) {
|
||||
@@ -526,6 +520,16 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
|
||||
_itemFilename = nameVal;
|
||||
_itemIsFile = true;
|
||||
}
|
||||
// Add the parameters from the content-disposition header to the param list, flagged as POST and File,
|
||||
// so that they can be retrieved using getParam(name, isPost=true, isFile=true)
|
||||
// in the upload handler to correctly handle multiple file uploads within the same request.
|
||||
// Example: Content-Disposition: form-data; name="fw"; filename="firmware.bin"
|
||||
// See: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/328
|
||||
if (_itemIsFile && _itemName.length() && _itemFilename.length()) {
|
||||
// add new parameters for this content-disposition
|
||||
_params.emplace_back(T_name, _itemName, true, true);
|
||||
_params.emplace_back(T_filename, _itemFilename, true, true);
|
||||
}
|
||||
}
|
||||
_temp = emptyString;
|
||||
} else {
|
||||
@@ -540,9 +544,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
|
||||
}
|
||||
_itemBuffer = (uint8_t *)malloc(RESPONSE_STREAM_BUFFER_SIZE);
|
||||
if (_itemBuffer == NULL) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
_multiParseState = PARSE_ERROR;
|
||||
abort();
|
||||
return;
|
||||
@@ -596,13 +598,15 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
|
||||
if (!_itemIsFile) {
|
||||
_params.emplace_back(_itemName, _itemValue, true);
|
||||
} else {
|
||||
if (_itemSize) {
|
||||
if (_handler) {
|
||||
_handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);
|
||||
}
|
||||
_itemBufferIndex = 0;
|
||||
_params.emplace_back(_itemName, _itemFilename, true, true, _itemSize);
|
||||
if (_handler) {
|
||||
_handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);
|
||||
}
|
||||
_itemBufferIndex = 0;
|
||||
_params.emplace_back(_itemName, _itemFilename, true, true, _itemSize);
|
||||
// remove previous occurrence(s) of content-disposition parameters for this upload
|
||||
_params.remove_if([this](const AsyncWebParameter &p) {
|
||||
return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename);
|
||||
});
|
||||
free(_itemBuffer);
|
||||
_itemBuffer = NULL;
|
||||
}
|
||||
@@ -707,7 +711,7 @@ void AsyncWebServerRequest::_runMiddlewareChain() {
|
||||
|
||||
void AsyncWebServerRequest::_send() {
|
||||
if (!_sent && !_paused) {
|
||||
// log_d("AsyncWebServerRequest::_send()");
|
||||
// async_ws_log_d("AsyncWebServerRequest::_send()");
|
||||
|
||||
// user did not create a response ?
|
||||
if (!_response) {
|
||||
@@ -719,7 +723,7 @@ void AsyncWebServerRequest::_send() {
|
||||
send(500, T_text_plain, "Invalid data in handler");
|
||||
}
|
||||
|
||||
// here, we either have a response give nfrom user or one of the two above
|
||||
// here, we either have a response given from user or one of the two above
|
||||
_client->setRxTimeout(0);
|
||||
_response->_respond(this);
|
||||
_sent = true;
|
||||
@@ -743,7 +747,7 @@ void AsyncWebServerRequest::abort() {
|
||||
_sent = true;
|
||||
_paused = false;
|
||||
_this.reset();
|
||||
// log_e("AsyncWebServerRequest::abort");
|
||||
// async_ws_log_e("AsyncWebServerRequest::abort");
|
||||
_client->abort();
|
||||
}
|
||||
}
|
||||
@@ -997,15 +1001,13 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
|
||||
case AsyncAuthType::AUTH_BASIC:
|
||||
{
|
||||
String header;
|
||||
if (header.reserve(strlen(T_BASIC_REALM) + strlen(realm) + 1)) {
|
||||
if (header.reserve(sizeof(T_BASIC_REALM) - 1 + strlen(realm) + 1)) {
|
||||
header.concat(T_BASIC_REALM);
|
||||
header.concat(realm);
|
||||
header.concat('"');
|
||||
r->addHeader(T_WWW_AUTH, header.c_str());
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
abort();
|
||||
}
|
||||
|
||||
@@ -1013,7 +1015,7 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
|
||||
}
|
||||
case AsyncAuthType::AUTH_DIGEST:
|
||||
{
|
||||
size_t len = strlen(T_DIGEST_) + strlen(T_realm__) + strlen(T_auth_nonce) + 32 + strlen(T__opaque) + 32 + 1;
|
||||
size_t len = sizeof(T_DIGEST_) - 1 + sizeof(T_realm__) - 1 + sizeof(T_auth_nonce) - 1 + 32 + sizeof(T__opaque) - 1 + 32 + 1;
|
||||
String header;
|
||||
if (header.reserve(len + strlen(realm))) {
|
||||
const String nonce = genRandomMD5();
|
||||
@@ -1029,9 +1031,7 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
|
||||
header.concat((char)0x22); // '"'
|
||||
r->addHeader(T_WWW_AUTH, header.c_str());
|
||||
} else {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
@@ -1081,15 +1081,6 @@ const String &AsyncWebServerRequest::argName(size_t i) const {
|
||||
return getParam(i)->name();
|
||||
}
|
||||
|
||||
const String &AsyncWebServerRequest::pathArg(size_t i) const {
|
||||
if (i >= _pathParams.size()) {
|
||||
return emptyString;
|
||||
}
|
||||
auto it = _pathParams.begin();
|
||||
std::advance(it, i);
|
||||
return *it;
|
||||
}
|
||||
|
||||
const String &AsyncWebServerRequest::header(const char *name) const {
|
||||
const AsyncWebHeader *h = getHeader(name);
|
||||
return h ? h->value() : emptyString;
|
||||
@@ -1118,9 +1109,7 @@ String AsyncWebServerRequest::urlDecode(const String &text) const {
|
||||
String decoded;
|
||||
// Allocate the string internal buffer - never longer from source text
|
||||
if (!decoded.reserve(len)) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
while (i < len) {
|
||||
@@ -1183,3 +1172,9 @@ bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType
|
||||
return ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) || ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype))
|
||||
|| ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype));
|
||||
}
|
||||
|
||||
AsyncClient *AsyncWebServerRequest::clientRelease() {
|
||||
AsyncClient *c = _client;
|
||||
_client = nullptr;
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,64 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#ifndef ASYNCWEBSERVERRESPONSEIMPL_H_
|
||||
#define ASYNCWEBSERVERRESPONSEIMPL_H_
|
||||
#pragma once
|
||||
|
||||
#ifdef Arduino_h
|
||||
// arduino is not compatible with std::vector
|
||||
#undef min
|
||||
#undef max
|
||||
#endif
|
||||
#include "literals.h"
|
||||
#include <cbuf.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "./literals.h"
|
||||
|
||||
#ifndef CONFIG_LWIP_TCP_MSS
|
||||
#ifdef TCP_MSS // ESP8266
|
||||
#define CONFIG_LWIP_TCP_MSS TCP_MSS
|
||||
#else
|
||||
// as it is defined for ESP32's Arduino LWIP
|
||||
#define CONFIG_LWIP_TCP_MSS 1436
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define ASYNC_RESPONCE_BUFF_SIZE CONFIG_LWIP_TCP_MSS * 2
|
||||
// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max.
|
||||
|
||||
class AsyncBasicResponse : public AsyncWebServerResponse {
|
||||
private:
|
||||
String _content;
|
||||
// buffer to accumulate all response headers
|
||||
String _assembled_headers;
|
||||
// amount of headers buffer writtent to sockbuff
|
||||
size_t _writtenHeadersLength{0};
|
||||
|
||||
public:
|
||||
explicit AsyncBasicResponse(int code, const char *contentType = asyncsrv::empty, const char *content = asyncsrv::empty);
|
||||
AsyncBasicResponse(int code, const String &contentType, const String &content = emptyString)
|
||||
: AsyncBasicResponse(code, contentType.c_str(), content.c_str()) {}
|
||||
void _respond(AsyncWebServerRequest *request) override final;
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final;
|
||||
bool _sourceValid() const override final {
|
||||
void _respond(AsyncWebServerRequest *request) final;
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) final {
|
||||
return write_send_buffs(request, len, time);
|
||||
};
|
||||
bool _sourceValid() const final {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief write next portion of response data to send buffs
|
||||
* this method (re)fills tcp send buffers, it could be called either at will
|
||||
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
|
||||
*
|
||||
* @param request - used to access client object
|
||||
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
|
||||
* @param time - time passed between last sent and received packet
|
||||
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
|
||||
*/
|
||||
size_t write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time);
|
||||
};
|
||||
|
||||
class AsyncAbstractResponse : public AsyncWebServerResponse {
|
||||
@@ -39,23 +69,43 @@ private:
|
||||
// in-flight queue credits
|
||||
size_t _in_flight_credit{2};
|
||||
#endif
|
||||
String _head;
|
||||
// buffer to accumulate all response headers
|
||||
String _assembled_headers;
|
||||
// amount of headers buffer writtent to sockbuff
|
||||
size_t _writtenHeadersLength{0};
|
||||
// Data is inserted into cache at begin().
|
||||
// This is inefficient with vector, but if we use some other container,
|
||||
// we won't be able to access it as contiguous array of bytes when reading from it,
|
||||
// so by gaining performance in one place, we'll lose it in another.
|
||||
std::vector<uint8_t> _cache;
|
||||
// intermediate buffer to copy outbound data to, also it will keep pending data between _send calls
|
||||
std::unique_ptr<std::array<uint8_t, ASYNC_RESPONCE_BUFF_SIZE> > _send_buffer;
|
||||
// buffer data size specifiers
|
||||
size_t _send_buffer_offset{0}, _send_buffer_len{0};
|
||||
size_t _readDataFromCacheOrContent(uint8_t *data, const size_t len);
|
||||
size_t _fillBufferAndProcessTemplates(uint8_t *buf, size_t maxLen);
|
||||
|
||||
protected:
|
||||
AwsTemplateProcessor _callback;
|
||||
/**
|
||||
* @brief write next portion of response data to send buffs
|
||||
* this method (re)fills tcp send buffers, it could be called either at will
|
||||
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
|
||||
*
|
||||
* @param request - used to access client object
|
||||
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
|
||||
* @param time - time passed between last sent and received packet
|
||||
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
|
||||
*/
|
||||
size_t write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time);
|
||||
|
||||
public:
|
||||
AsyncAbstractResponse(AwsTemplateProcessor callback = nullptr);
|
||||
virtual ~AsyncAbstractResponse() {}
|
||||
void _respond(AsyncWebServerRequest *request) override final;
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final;
|
||||
void _respond(AsyncWebServerRequest *request) final;
|
||||
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) final {
|
||||
return write_send_buffs(request, len, time);
|
||||
};
|
||||
virtual bool _sourceValid() const {
|
||||
return false;
|
||||
}
|
||||
@@ -89,10 +139,10 @@ public:
|
||||
~AsyncFileResponse() {
|
||||
_content.close();
|
||||
}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return !!(_content);
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
};
|
||||
|
||||
class AsyncStreamResponse : public AsyncAbstractResponse {
|
||||
@@ -103,10 +153,10 @@ public:
|
||||
AsyncStreamResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback = nullptr);
|
||||
AsyncStreamResponse(Stream &stream, const String &contentType, size_t len, AwsTemplateProcessor callback = nullptr)
|
||||
: AsyncStreamResponse(stream, contentType.c_str(), len, callback) {}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return !!(_content);
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
};
|
||||
|
||||
class AsyncCallbackResponse : public AsyncAbstractResponse {
|
||||
@@ -118,10 +168,10 @@ public:
|
||||
AsyncCallbackResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
|
||||
AsyncCallbackResponse(const String &contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr)
|
||||
: AsyncCallbackResponse(contentType.c_str(), len, callback, templateCallback) {}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return !!(_content);
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
};
|
||||
|
||||
class AsyncChunkedResponse : public AsyncAbstractResponse {
|
||||
@@ -133,25 +183,26 @@ public:
|
||||
AsyncChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
|
||||
AsyncChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr)
|
||||
: AsyncChunkedResponse(contentType.c_str(), callback, templateCallback) {}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return !!(_content);
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
};
|
||||
|
||||
class AsyncProgmemResponse : public AsyncAbstractResponse {
|
||||
private:
|
||||
const uint8_t *_content;
|
||||
size_t _readLength;
|
||||
// offset index (how much we've sent already)
|
||||
size_t _index;
|
||||
|
||||
public:
|
||||
AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr);
|
||||
AsyncProgmemResponse(int code, const String &contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr)
|
||||
: AsyncProgmemResponse(code, contentType.c_str(), content, len, callback) {}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return true;
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
};
|
||||
|
||||
class AsyncResponseStream : public AsyncAbstractResponse, public Print {
|
||||
@@ -161,10 +212,10 @@ private:
|
||||
public:
|
||||
AsyncResponseStream(const char *contentType, size_t bufferSize);
|
||||
AsyncResponseStream(const String &contentType, size_t bufferSize) : AsyncResponseStream(contentType.c_str(), bufferSize) {}
|
||||
bool _sourceValid() const override final {
|
||||
bool _sourceValid() const final {
|
||||
return (_state < RESPONSE_END);
|
||||
}
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
|
||||
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
|
||||
size_t write(const uint8_t *data, size_t len);
|
||||
size_t write(uint8_t data);
|
||||
/**
|
||||
@@ -175,5 +226,3 @@ public:
|
||||
}
|
||||
using Print::write;
|
||||
};
|
||||
|
||||
#endif /* ASYNCWEBSERVERRESPONSEIMPL_H_ */
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebResponseImpl.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#ifndef CONFIG_LWIP_TCP_WND_DEFAULT
|
||||
#ifdef TCP_WND // ESP8266
|
||||
#define CONFIG_LWIP_TCP_WND_DEFAULT TCP_WND
|
||||
#else
|
||||
// as it is defined for esp32's LWIP
|
||||
#define CONFIG_LWIP_TCP_WND_DEFAULT 5760
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
@@ -11,50 +25,50 @@ using namespace asyncsrv;
|
||||
*
|
||||
*/
|
||||
|
||||
const char *AsyncWebServerResponse::responseCodeToString(int code) {
|
||||
STR_RETURN_TYPE AsyncWebServerResponse::responseCodeToString(int code) {
|
||||
switch (code) {
|
||||
case 100: return T_HTTP_CODE_100;
|
||||
case 101: return T_HTTP_CODE_101;
|
||||
case 200: return T_HTTP_CODE_200;
|
||||
case 201: return T_HTTP_CODE_201;
|
||||
case 202: return T_HTTP_CODE_202;
|
||||
case 203: return T_HTTP_CODE_203;
|
||||
case 204: return T_HTTP_CODE_204;
|
||||
case 205: return T_HTTP_CODE_205;
|
||||
case 206: return T_HTTP_CODE_206;
|
||||
case 300: return T_HTTP_CODE_300;
|
||||
case 301: return T_HTTP_CODE_301;
|
||||
case 302: return T_HTTP_CODE_302;
|
||||
case 303: return T_HTTP_CODE_303;
|
||||
case 304: return T_HTTP_CODE_304;
|
||||
case 305: return T_HTTP_CODE_305;
|
||||
case 307: return T_HTTP_CODE_307;
|
||||
case 400: return T_HTTP_CODE_400;
|
||||
case 401: return T_HTTP_CODE_401;
|
||||
case 402: return T_HTTP_CODE_402;
|
||||
case 403: return T_HTTP_CODE_403;
|
||||
case 404: return T_HTTP_CODE_404;
|
||||
case 405: return T_HTTP_CODE_405;
|
||||
case 406: return T_HTTP_CODE_406;
|
||||
case 407: return T_HTTP_CODE_407;
|
||||
case 408: return T_HTTP_CODE_408;
|
||||
case 409: return T_HTTP_CODE_409;
|
||||
case 410: return T_HTTP_CODE_410;
|
||||
case 411: return T_HTTP_CODE_411;
|
||||
case 412: return T_HTTP_CODE_412;
|
||||
case 413: return T_HTTP_CODE_413;
|
||||
case 414: return T_HTTP_CODE_414;
|
||||
case 415: return T_HTTP_CODE_415;
|
||||
case 416: return T_HTTP_CODE_416;
|
||||
case 417: return T_HTTP_CODE_417;
|
||||
case 429: return T_HTTP_CODE_429;
|
||||
case 500: return T_HTTP_CODE_500;
|
||||
case 501: return T_HTTP_CODE_501;
|
||||
case 502: return T_HTTP_CODE_502;
|
||||
case 503: return T_HTTP_CODE_503;
|
||||
case 504: return T_HTTP_CODE_504;
|
||||
case 505: return T_HTTP_CODE_505;
|
||||
default: return T_HTTP_CODE_ANY;
|
||||
case 100: return STR(T_HTTP_CODE_100);
|
||||
case 101: return STR(T_HTTP_CODE_101);
|
||||
case 200: return STR(T_HTTP_CODE_200);
|
||||
case 201: return STR(T_HTTP_CODE_201);
|
||||
case 202: return STR(T_HTTP_CODE_202);
|
||||
case 203: return STR(T_HTTP_CODE_203);
|
||||
case 204: return STR(T_HTTP_CODE_204);
|
||||
case 205: return STR(T_HTTP_CODE_205);
|
||||
case 206: return STR(T_HTTP_CODE_206);
|
||||
case 300: return STR(T_HTTP_CODE_300);
|
||||
case 301: return STR(T_HTTP_CODE_301);
|
||||
case 302: return STR(T_HTTP_CODE_302);
|
||||
case 303: return STR(T_HTTP_CODE_303);
|
||||
case 304: return STR(T_HTTP_CODE_304);
|
||||
case 305: return STR(T_HTTP_CODE_305);
|
||||
case 307: return STR(T_HTTP_CODE_307);
|
||||
case 400: return STR(T_HTTP_CODE_400);
|
||||
case 401: return STR(T_HTTP_CODE_401);
|
||||
case 402: return STR(T_HTTP_CODE_402);
|
||||
case 403: return STR(T_HTTP_CODE_403);
|
||||
case 404: return STR(T_HTTP_CODE_404);
|
||||
case 405: return STR(T_HTTP_CODE_405);
|
||||
case 406: return STR(T_HTTP_CODE_406);
|
||||
case 407: return STR(T_HTTP_CODE_407);
|
||||
case 408: return STR(T_HTTP_CODE_408);
|
||||
case 409: return STR(T_HTTP_CODE_409);
|
||||
case 410: return STR(T_HTTP_CODE_410);
|
||||
case 411: return STR(T_HTTP_CODE_411);
|
||||
case 412: return STR(T_HTTP_CODE_412);
|
||||
case 413: return STR(T_HTTP_CODE_413);
|
||||
case 414: return STR(T_HTTP_CODE_414);
|
||||
case 415: return STR(T_HTTP_CODE_415);
|
||||
case 416: return STR(T_HTTP_CODE_416);
|
||||
case 417: return STR(T_HTTP_CODE_417);
|
||||
case 429: return STR(T_HTTP_CODE_429);
|
||||
case 500: return STR(T_HTTP_CODE_500);
|
||||
case 501: return STR(T_HTTP_CODE_501);
|
||||
case 502: return STR(T_HTTP_CODE_502);
|
||||
case 503: return STR(T_HTTP_CODE_503);
|
||||
case 504: return STR(T_HTTP_CODE_504);
|
||||
case 505: return STR(T_HTTP_CODE_505);
|
||||
default: return STR(T_HTTP_CODE_ANY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,13 +251,6 @@ bool AsyncWebServerResponse::_sourceValid() const {
|
||||
}
|
||||
void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request) {
|
||||
_state = RESPONSE_END;
|
||||
request->client()->close();
|
||||
}
|
||||
size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
(void)request;
|
||||
(void)len;
|
||||
(void)time;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -264,70 +271,61 @@ AsyncBasicResponse::AsyncBasicResponse(int code, const char *contentType, const
|
||||
|
||||
void AsyncBasicResponse::_respond(AsyncWebServerRequest *request) {
|
||||
_state = RESPONSE_HEADERS;
|
||||
String out;
|
||||
_assembleHead(out, request->version());
|
||||
size_t outLen = out.length();
|
||||
size_t space = request->client()->space();
|
||||
if (!_contentLength && space >= outLen) {
|
||||
_writtenLength += request->client()->write(out.c_str(), outLen);
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
} else if (_contentLength && space >= outLen + _contentLength) {
|
||||
out += _content;
|
||||
outLen += _contentLength;
|
||||
_writtenLength += request->client()->write(out.c_str(), outLen);
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
} else if (space && space < outLen) {
|
||||
String partial = out.substring(0, space);
|
||||
_content = out.substring(space) + _content;
|
||||
_contentLength += outLen - space;
|
||||
_writtenLength += request->client()->write(partial.c_str(), partial.length());
|
||||
_state = RESPONSE_CONTENT;
|
||||
} else if (space > outLen && space < (outLen + _contentLength)) {
|
||||
size_t shift = space - outLen;
|
||||
outLen += shift;
|
||||
_sentLength += shift;
|
||||
out += _content.substring(0, shift);
|
||||
_content = _content.substring(shift);
|
||||
_writtenLength += request->client()->write(out.c_str(), outLen);
|
||||
_state = RESPONSE_CONTENT;
|
||||
} else {
|
||||
_content = out + _content;
|
||||
_contentLength += outLen;
|
||||
_state = RESPONSE_CONTENT;
|
||||
}
|
||||
_assembleHead(_assembled_headers, request->version());
|
||||
write_send_buffs(request, 0, 0);
|
||||
}
|
||||
|
||||
size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
size_t AsyncBasicResponse::write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
(void)time;
|
||||
|
||||
// this is not functionally needed in AsyncBasicResponse itself, but kept for compatibility if some of the derived classes are rely on it somehow
|
||||
_ackedLength += len;
|
||||
if (_state == RESPONSE_CONTENT) {
|
||||
size_t available = _contentLength - _sentLength;
|
||||
size_t space = request->client()->space();
|
||||
// we can fit in this packet
|
||||
if (space > available) {
|
||||
_writtenLength += request->client()->write(_content.c_str(), available);
|
||||
_content = emptyString;
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
return available;
|
||||
size_t payloadlen{0}; // amount of data to be written to tcp sockbuff during this call, used as return value of this method
|
||||
|
||||
// send http headers first
|
||||
if (_state == RESPONSE_HEADERS) {
|
||||
// copy headers buffer to sock buffer
|
||||
size_t const pcb_written = request->client()->add(_assembled_headers.c_str() + _writtenHeadersLength, _assembled_headers.length() - _writtenHeadersLength);
|
||||
_writtenLength += pcb_written;
|
||||
_writtenHeadersLength += pcb_written;
|
||||
if (_writtenHeadersLength < _assembled_headers.length()) {
|
||||
// we were not able to fit all headers in current buff, send this part here and return later for the rest
|
||||
if (!request->client()->send()) {
|
||||
// something is wrong, what should we do here?
|
||||
request->client()->close();
|
||||
return 0;
|
||||
}
|
||||
return pcb_written;
|
||||
}
|
||||
// send some data, the rest on ack
|
||||
String out = _content.substring(0, space);
|
||||
_content = _content.substring(space);
|
||||
_sentLength += space;
|
||||
_writtenLength += request->client()->write(out.c_str(), space);
|
||||
return space;
|
||||
} else if (_state == RESPONSE_WAIT_ACK) {
|
||||
if (_ackedLength >= _writtenLength) {
|
||||
// otherwise we've added all the (remainder) headers in current buff, go on with content
|
||||
_state = RESPONSE_CONTENT;
|
||||
payloadlen += pcb_written;
|
||||
_assembled_headers = String(); // clear
|
||||
}
|
||||
|
||||
if (_state == RESPONSE_CONTENT) {
|
||||
size_t const pcb_written = request->client()->write(_content.c_str() + _sentLength, _content.length() - _sentLength);
|
||||
_writtenLength += pcb_written; // total written data (hdrs + body)
|
||||
_sentLength += pcb_written; // body written data
|
||||
payloadlen += pcb_written; // data writtent in current buff
|
||||
if (_sentLength >= _content.length()) {
|
||||
// we've just sent all the (remainder) data in current buff, complete the response
|
||||
_state = RESPONSE_END;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
// implicit complete
|
||||
if (_state == RESPONSE_WAIT_ACK) {
|
||||
_state = RESPONSE_END;
|
||||
}
|
||||
|
||||
return payloadlen;
|
||||
}
|
||||
|
||||
/*
|
||||
* Abstract Response
|
||||
* */
|
||||
|
||||
*
|
||||
*/
|
||||
AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _callback(callback) {
|
||||
// In case of template processing, we're unable to determine real response size
|
||||
if (callback) {
|
||||
@@ -339,12 +337,12 @@ AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _c
|
||||
|
||||
void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request) {
|
||||
addHeader(T_Connection, T_close, false);
|
||||
_assembleHead(_head, request->version());
|
||||
_assembleHead(_assembled_headers, request->version());
|
||||
_state = RESPONSE_HEADERS;
|
||||
_ack(request, 0, 0);
|
||||
write_send_buffs(request, 0, 0);
|
||||
}
|
||||
|
||||
size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time) {
|
||||
(void)time;
|
||||
if (!_sourceValid()) {
|
||||
_state = RESPONSE_FAILED;
|
||||
@@ -353,142 +351,168 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
|
||||
}
|
||||
|
||||
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
/*
|
||||
for response payloads with unknown length or length larger than TCP_WND we need to control AsyncTCP's queue and in-flight fragmentation.
|
||||
Either user callback could fill buffer with very small chunks or long running large response could receive a lot of poll() calls here,
|
||||
both could flood asynctcp's queue with large number of events to handle and fragment socket buffer space for large responses.
|
||||
Let's ignore polled acks and acks in case when available window size is less than our used buffer size since we won't be able to fill and send it whole
|
||||
That way we could balance on having at least half tcp win in-flight while minimizing send/ack events in asynctcp Q
|
||||
This could decrease sustained bandwidth for one single connection but would drastically improve parallelism and equalize bandwidth sharing
|
||||
*/
|
||||
// return a credit for each chunk of acked data (polls does not give any credits)
|
||||
if (len) {
|
||||
++_in_flight_credit;
|
||||
_in_flight -= std::min(len, _in_flight);
|
||||
}
|
||||
|
||||
// for chunked responses ignore acks if there are no _in_flight_credits left
|
||||
if (_chunked && !_in_flight_credit) {
|
||||
#ifdef ESP32
|
||||
log_d("(chunk) out of in-flight credits");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
_in_flight -= (_in_flight > len) ? len : _in_flight;
|
||||
// get the size of available sock space
|
||||
#endif
|
||||
|
||||
_ackedLength += len;
|
||||
size_t space = request->client()->space();
|
||||
|
||||
size_t headLen = _head.length();
|
||||
if (_state == RESPONSE_HEADERS) {
|
||||
if (space >= headLen) {
|
||||
_state = RESPONSE_CONTENT;
|
||||
space -= headLen;
|
||||
} else {
|
||||
String out = _head.substring(0, space);
|
||||
_head = _head.substring(space);
|
||||
_writtenLength += request->client()->write(out.c_str(), out.length());
|
||||
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
_in_flight += out.length();
|
||||
--_in_flight_credit; // take a credit
|
||||
#endif
|
||||
return out.length();
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == RESPONSE_CONTENT) {
|
||||
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
// for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency,
|
||||
// but flood asynctcp's queue and fragment socket buffer space for large responses.
|
||||
// Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space.
|
||||
// That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q
|
||||
if (_in_flight > space) {
|
||||
// log_d("defer user call %u/%u", _in_flight, space);
|
||||
// take the credit back since we are ignoring this ack and rely on other inflight data
|
||||
if (_chunked || !_sendContentLength || (_sentLength > CONFIG_LWIP_TCP_WND_DEFAULT)) {
|
||||
if (!_in_flight_credit || (ASYNC_RESPONCE_BUFF_SIZE > request->client()->space())) {
|
||||
// async_ws_log_d("defer user call in_flight:%u, tcpwin:%u", _in_flight, request->client()->space());
|
||||
// take the credit back since we are ignoring this ack and rely on other inflight data acks
|
||||
if (len) {
|
||||
--_in_flight_credit;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t outLen;
|
||||
if (_chunked) {
|
||||
if (space <= 8) {
|
||||
return 0;
|
||||
}
|
||||
// this is not functionally needed in AsyncAbstractResponse itself, but kept for compatibility if some of the derived classes are rely on it somehow
|
||||
_ackedLength += len;
|
||||
|
||||
outLen = space;
|
||||
} else if (!_sendContentLength) {
|
||||
outLen = space;
|
||||
} else {
|
||||
outLen = ((_contentLength - _sentLength) > space) ? space : (_contentLength - _sentLength);
|
||||
}
|
||||
size_t payloadlen{0}; // amount of data to be written to tcp sockbuff during this call, used as return value of this method
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(outLen + headLen);
|
||||
if (!buf) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
request->abort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (headLen) {
|
||||
memcpy(buf, _head.c_str(), _head.length());
|
||||
}
|
||||
|
||||
size_t readLen = 0;
|
||||
|
||||
if (_chunked) {
|
||||
// HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added.
|
||||
// See RFC2616 sections 2, 3.6.1.
|
||||
readLen = _fillBufferAndProcessTemplates(buf + headLen + 6, outLen - 8);
|
||||
if (readLen == RESPONSE_TRY_AGAIN) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
outLen = sprintf((char *)buf + headLen, "%04x", readLen) + headLen;
|
||||
buf[outLen++] = '\r';
|
||||
buf[outLen++] = '\n';
|
||||
outLen += readLen;
|
||||
buf[outLen++] = '\r';
|
||||
buf[outLen++] = '\n';
|
||||
} else {
|
||||
readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen);
|
||||
if (readLen == RESPONSE_TRY_AGAIN) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
outLen = readLen + headLen;
|
||||
}
|
||||
|
||||
if (headLen) {
|
||||
_head = emptyString;
|
||||
}
|
||||
|
||||
if (outLen) {
|
||||
_writtenLength += request->client()->write((const char *)buf, outLen);
|
||||
// send http headers first
|
||||
if (_state == RESPONSE_HEADERS) {
|
||||
// copy headers buffer to sock buffer
|
||||
size_t const pcb_written = request->client()->add(_assembled_headers.c_str() + _writtenHeadersLength, _assembled_headers.length() - _writtenHeadersLength);
|
||||
_writtenLength += pcb_written;
|
||||
_writtenHeadersLength += pcb_written;
|
||||
if (_writtenHeadersLength < _assembled_headers.length()) {
|
||||
// we were not able to fit all headers in current buff, send this part here and return later for the rest
|
||||
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
_in_flight += outLen;
|
||||
_in_flight += pcb_written;
|
||||
--_in_flight_credit; // take a credit
|
||||
#endif
|
||||
}
|
||||
|
||||
if (_chunked) {
|
||||
_sentLength += readLen;
|
||||
} else {
|
||||
_sentLength += outLen - headLen;
|
||||
}
|
||||
|
||||
free(buf);
|
||||
|
||||
if ((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)) {
|
||||
_state = RESPONSE_WAIT_ACK;
|
||||
}
|
||||
return outLen;
|
||||
|
||||
} else if (_state == RESPONSE_WAIT_ACK) {
|
||||
if (!_sendContentLength || _ackedLength >= _writtenLength) {
|
||||
_state = RESPONSE_END;
|
||||
if (!_chunked && !_sendContentLength) {
|
||||
request->client()->close(true);
|
||||
if (!request->client()->send()) {
|
||||
// something is wrong, what should we do here?
|
||||
request->client()->close();
|
||||
return 0;
|
||||
}
|
||||
return pcb_written;
|
||||
}
|
||||
// otherwise we've added all the (remainder) headers in current buff
|
||||
_state = RESPONSE_CONTENT;
|
||||
payloadlen += pcb_written;
|
||||
_assembled_headers = String(); // clear
|
||||
}
|
||||
|
||||
// send content body
|
||||
if (_state == RESPONSE_CONTENT) {
|
||||
do {
|
||||
if (_send_buffer_len && _send_buffer) {
|
||||
// data is pending in buffer from a previous call or previous iteration
|
||||
size_t const added_len =
|
||||
request->client()->add(reinterpret_cast<char *>(_send_buffer->data() + _send_buffer_offset), _send_buffer_len - _send_buffer_offset);
|
||||
if (added_len != _send_buffer_len - _send_buffer_offset) {
|
||||
// we were not able to add entire buffer's content to tcp buffs, leave it for later
|
||||
// (this should not happen normally unless connection's TCP window suddenly changed from remote or mem pressure)
|
||||
_send_buffer_offset += added_len;
|
||||
break;
|
||||
} else {
|
||||
_send_buffer_len = _send_buffer_offset = 0; // consider buffer empty
|
||||
}
|
||||
payloadlen += added_len;
|
||||
}
|
||||
|
||||
auto tcp_win = request->client()->space();
|
||||
if (tcp_win == 0 || _state == RESPONSE_END) {
|
||||
break; // no room left or no more data
|
||||
}
|
||||
|
||||
if ((_chunked || !_sendContentLength) && (tcp_win < CONFIG_LWIP_TCP_MSS / 2)) {
|
||||
// available window size is not enough to send a new chunk sized half of tcp mss, let's wait for better chance and reduce pressure to AsyncTCP's event Q
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_send_buffer) {
|
||||
auto p = new (std::nothrow) std::array<uint8_t, ASYNC_RESPONCE_BUFF_SIZE>;
|
||||
if (p) {
|
||||
_send_buffer.reset(p);
|
||||
_send_buffer_len = _send_buffer_offset = 0;
|
||||
} else {
|
||||
break; // OOM
|
||||
}
|
||||
}
|
||||
|
||||
if (_chunked) {
|
||||
// HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added.
|
||||
// See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1
|
||||
size_t const readLen =
|
||||
_fillBufferAndProcessTemplates(_send_buffer->data() + 6, std::min(_send_buffer->size(), tcp_win) - 8); // reserve 8 bytes for chunk size data
|
||||
if (readLen != RESPONSE_TRY_AGAIN) {
|
||||
// Write 4 hex digits directly without null terminator
|
||||
static constexpr char hexChars[] = "0123456789abcdef";
|
||||
_send_buffer->data()[0] = hexChars[(readLen >> 12) & 0xF];
|
||||
_send_buffer->data()[1] = hexChars[(readLen >> 8) & 0xF];
|
||||
_send_buffer->data()[2] = hexChars[(readLen >> 4) & 0xF];
|
||||
_send_buffer->data()[3] = hexChars[readLen & 0xF];
|
||||
_send_buffer->data()[4] = '\r';
|
||||
_send_buffer->data()[5] = '\n';
|
||||
// data (readLen bytes) is already there
|
||||
_send_buffer->at(readLen + 6) = '\r';
|
||||
_send_buffer->at(readLen + 7) = '\n';
|
||||
_send_buffer_len += readLen + 8; // set buffers's size to match added data
|
||||
_sentLength += readLen; // data is not sent yet, but we won't get a chance to count this later properly for chunked data
|
||||
if (!readLen) {
|
||||
// last chunk?
|
||||
_state = RESPONSE_END;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-chunked data. We can either have a response:
|
||||
// - with a known content-length (example: Json response), in that case we pass the remaining length if lower than tcp_win
|
||||
// - or with unknown content-length (see LargeResponse example, like ESP32Cam with streaming), in that case we just fill as much as tcp_win allows
|
||||
size_t maxLen = std::min(_send_buffer->size(), tcp_win);
|
||||
if (_contentLength) {
|
||||
maxLen = _contentLength > _sentLength ? std::min(maxLen, _contentLength - _sentLength) : 0;
|
||||
}
|
||||
|
||||
size_t const readLen = _fillBufferAndProcessTemplates(_send_buffer->data(), maxLen);
|
||||
|
||||
if (readLen == 0) {
|
||||
// no more data to send
|
||||
_state = RESPONSE_END;
|
||||
} else if (readLen != RESPONSE_TRY_AGAIN) {
|
||||
_send_buffer_len += readLen; // set buffers's size to match added data
|
||||
_sentLength += readLen; // data is not sent yet, but we need it to understand that it would be last block
|
||||
if (_sendContentLength && (_sentLength == _contentLength)) {
|
||||
// it was last piece of content
|
||||
_state = RESPONSE_END;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (_send_buffer_len); // go on till we have something in buffer pending to send
|
||||
|
||||
// execute sending whatever we have in sock buffs now
|
||||
request->client()->send();
|
||||
_writtenLength += payloadlen;
|
||||
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
|
||||
_in_flight += payloadlen;
|
||||
--_in_flight_credit; // take a credit
|
||||
#endif
|
||||
if (_send_buffer_len == 0) {
|
||||
// buffer empty, we can release mem, otherwise need to keep it till next run (should not happen under normal conditions)
|
||||
_send_buffer.reset();
|
||||
}
|
||||
return payloadlen;
|
||||
} // (_state == RESPONSE_CONTENT)
|
||||
|
||||
// implicit check
|
||||
if (_state == RESPONSE_WAIT_ACK) {
|
||||
// we do not need to wait for any acks actually if we won't send any more data,
|
||||
// connection would be closed gracefully with last piece of data (in AsyncWebServerRequest::_onAck)
|
||||
_state = RESPONSE_END;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -516,8 +540,8 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size
|
||||
// Now we've read 'len' bytes, either from cache or from file
|
||||
// Search for template placeholders
|
||||
uint8_t *pTemplateStart = data;
|
||||
while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))
|
||||
) { // data[0] ... data[len - 1]
|
||||
while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) {
|
||||
// data[0] ... data[len - 1]
|
||||
uint8_t *pTemplateEnd =
|
||||
(pTemplateStart < &data[len - 1]) ? (uint8_t *)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr;
|
||||
// temporary buffer to hold parameter name
|
||||
@@ -639,8 +663,8 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
|
||||
_contentType = T_text_html;
|
||||
} else if (strcmp(dot, T__css) == 0) {
|
||||
_contentType = T_text_css;
|
||||
} else if (strcmp(dot, T__js) == 0) {
|
||||
_contentType = T_application_javascript;
|
||||
} else if (strcmp(dot, T__js) == 0 || strcmp(dot, T__mjs) == 0) {
|
||||
_contentType = T_text_javascript;
|
||||
} else if (strcmp(dot, T__json) == 0) {
|
||||
_contentType = T_application_json;
|
||||
} else if (strcmp(dot, T__png) == 0) {
|
||||
@@ -699,29 +723,23 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
|
||||
|
||||
// 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)
|
||||
if (!_content.available()) {
|
||||
// If not available 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)) {
|
||||
char serverETag[11];
|
||||
if (AsyncWebServerRequest::_getEtag(_content, serverETag)) {
|
||||
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);
|
||||
|
||||
@@ -733,6 +751,8 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
|
||||
}
|
||||
}
|
||||
|
||||
_contentLength = _content.size();
|
||||
|
||||
if (*contentType == '\0') {
|
||||
_setContentTypeFromPath(path);
|
||||
} else {
|
||||
@@ -742,9 +762,12 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
|
||||
if (download) {
|
||||
// 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);
|
||||
const char *filename = path.c_str() + filenameStart;
|
||||
String buf;
|
||||
buf.reserve(sizeof(T_attachment) - 1 + strlen(filename) + 2);
|
||||
buf = T_attachment;
|
||||
buf += filename;
|
||||
buf += "\"";
|
||||
addHeader(T_Content_Disposition, buf, false);
|
||||
} else {
|
||||
// Serve file inline (display in browser)
|
||||
@@ -768,22 +791,26 @@ AsyncFileResponse::AsyncFileResponse(File content, const String &path, const cha
|
||||
_content = content;
|
||||
_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) {
|
||||
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
|
||||
// Extract filename from path and set as download attachment
|
||||
int filenameStart = path.lastIndexOf('/') + 1;
|
||||
const char *filename = path.c_str() + filenameStart;
|
||||
String buf;
|
||||
buf.reserve(sizeof(T_attachment) - 1 + strlen(filename) + 2);
|
||||
buf = T_attachment;
|
||||
buf += filename;
|
||||
buf += "\"";
|
||||
addHeader(T_Content_Disposition, buf, false);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
@@ -863,24 +890,17 @@ size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
* */
|
||||
|
||||
AsyncProgmemResponse::AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback)
|
||||
: AsyncAbstractResponse(callback) {
|
||||
: AsyncAbstractResponse(callback), _content(content), _index(0) {
|
||||
_code = code;
|
||||
_content = content;
|
||||
_contentType = contentType;
|
||||
_contentLength = len;
|
||||
_readLength = 0;
|
||||
}
|
||||
|
||||
size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len) {
|
||||
size_t left = _contentLength - _readLength;
|
||||
if (left > len) {
|
||||
memcpy_P(data, _content + _readLength, len);
|
||||
_readLength += len;
|
||||
return len;
|
||||
}
|
||||
memcpy_P(data, _content + _readLength, left);
|
||||
_readLength += left;
|
||||
return left;
|
||||
size_t read_size = std::min(len, _contentLength - _index);
|
||||
memcpy_P(data, _content + _index, read_size);
|
||||
_index += read_size;
|
||||
return read_size;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -894,9 +914,7 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
|
||||
// internal buffer will be null on allocation failure
|
||||
_content = std::unique_ptr<cbuf>(new cbuf(bufferSize));
|
||||
if (bufferSize && _content->size() < bufferSize) {
|
||||
#ifdef ESP32
|
||||
log_e("Failed to allocate");
|
||||
#endif
|
||||
async_ws_log_e("Failed to allocate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,9 +933,7 @@ size_t AsyncResponseStream::write(const uint8_t *data, size_t len) {
|
||||
// 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
|
||||
async_ws_log_e("Failed to allocate");
|
||||
}
|
||||
}
|
||||
size_t written = _content->write((const char *)data, len);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebHandlerImpl.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
@@ -15,7 +18,7 @@
|
||||
using namespace asyncsrv;
|
||||
|
||||
bool ON_STA_FILTER(AsyncWebServerRequest *request) {
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
return WiFi.localIP() == request->client()->localIP();
|
||||
#else
|
||||
return false;
|
||||
@@ -23,7 +26,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) {
|
||||
}
|
||||
|
||||
bool ON_AP_FILTER(AsyncWebServerRequest *request) {
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
return WiFi.localIP() != request->client()->localIP();
|
||||
#else
|
||||
return false;
|
||||
@@ -151,10 +154,10 @@ void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request) {
|
||||
}
|
||||
|
||||
AsyncCallbackWebHandler &AsyncWebServer::on(
|
||||
const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody
|
||||
AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody
|
||||
) {
|
||||
AsyncCallbackWebHandler *handler = new AsyncCallbackWebHandler();
|
||||
handler->setUri(uri);
|
||||
handler->setUri(std::move(uri));
|
||||
handler->setMethod(method);
|
||||
handler->onRequest(onRequest);
|
||||
handler->onUpload(onUpload);
|
||||
@@ -163,6 +166,15 @@ AsyncCallbackWebHandler &AsyncWebServer::on(
|
||||
return *handler;
|
||||
}
|
||||
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
AsyncCallbackJsonWebHandler &AsyncWebServer::on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody) {
|
||||
AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler(std::move(uri), onBody);
|
||||
handler->setMethod(method);
|
||||
addHandler(handler);
|
||||
return *handler;
|
||||
}
|
||||
#endif
|
||||
|
||||
AsyncStaticWebHandler &AsyncWebServer::serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control) {
|
||||
AsyncStaticWebHandler *handler = new AsyncStaticWebHandler(uri, fs, path, cache_control);
|
||||
addHandler(handler);
|
||||
@@ -193,3 +205,141 @@ void AsyncWebServer::reset() {
|
||||
_catchAllHandler->onUpload(NULL);
|
||||
_catchAllHandler->onBody(NULL);
|
||||
}
|
||||
|
||||
AsyncURIMatcher::AsyncURIMatcher(String uri, uint16_t modifiers) : _value(std::move(uri)) {
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
if (_value.startsWith("^") && _value.endsWith("$")) {
|
||||
pattern = new std::regex(_value.c_str(), (modifiers & CaseInsensitive) ? (std::regex::icase | std::regex::optimize) : (std::regex::optimize));
|
||||
return; // no additional processing - flags are overwritten by pattern pointer
|
||||
}
|
||||
#endif
|
||||
if (modifiers & CaseInsensitive) {
|
||||
_value.toLowerCase();
|
||||
}
|
||||
// Inspect _value to set flags
|
||||
// empty URI matches everything
|
||||
if (!_value.length()) {
|
||||
_flags = _toFlags(Type::All, modifiers);
|
||||
} else if (_value.endsWith("*")) {
|
||||
// wildcard match with * at the end
|
||||
_flags = _toFlags(Type::Prefix, modifiers);
|
||||
_value = _value.substring(0, _value.length() - 1);
|
||||
} else if (_value.lastIndexOf("/*.") >= 0) {
|
||||
// prefix match with /*.ext
|
||||
// matches any path ending with .ext
|
||||
// e.g. /images/*.png will match /images/pic.png and /images/2023/pic.png but not /img/pic.png
|
||||
_flags = _toFlags(Type::Extension, modifiers);
|
||||
} else {
|
||||
// backward compatible use case: exact match or prefix with trailing /
|
||||
_flags = _toFlags(Type::BackwardCompatible, modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
AsyncURIMatcher::AsyncURIMatcher(String uri, Type type, uint16_t modifiers) : _value(std::move(uri)), _flags(_toFlags(type, modifiers)) {
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
if (type == Type::Regex) {
|
||||
pattern = new std::regex(_value.c_str(), (modifiers & CaseInsensitive) ? (std::regex::icase | std::regex::optimize) : (std::regex::optimize));
|
||||
return; // no additional processing - flags are overwritten by pattern pointer
|
||||
}
|
||||
#endif
|
||||
if (modifiers & CaseInsensitive) {
|
||||
_value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
|
||||
AsyncURIMatcher::AsyncURIMatcher(const AsyncURIMatcher &c) : _value(c._value), _flags(c._flags) {
|
||||
if (_isRegex()) {
|
||||
pattern = new std::regex(*pattern);
|
||||
}
|
||||
}
|
||||
|
||||
AsyncURIMatcher::AsyncURIMatcher(AsyncURIMatcher &&c) : _value(std::move(c._value)), _flags(c._flags) {
|
||||
c._flags = _toFlags(Type::None, None);
|
||||
}
|
||||
|
||||
AsyncURIMatcher::~AsyncURIMatcher() {
|
||||
if (_isRegex()) {
|
||||
delete pattern;
|
||||
}
|
||||
}
|
||||
|
||||
AsyncURIMatcher &AsyncURIMatcher::operator=(const AsyncURIMatcher &r) {
|
||||
_value = r._value;
|
||||
if (r._isRegex()) {
|
||||
// Allocate first before we delete our current state
|
||||
auto p = new std::regex(*r.pattern);
|
||||
// Safely reassign our pattern
|
||||
if (_isRegex()) {
|
||||
delete pattern;
|
||||
}
|
||||
pattern = p;
|
||||
} else {
|
||||
if (_isRegex()) {
|
||||
delete pattern;
|
||||
}
|
||||
_flags = r._flags;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncURIMatcher &AsyncURIMatcher::operator=(AsyncURIMatcher &&r) {
|
||||
_value = std::move(r._value);
|
||||
if (_isRegex()) {
|
||||
delete pattern;
|
||||
}
|
||||
_flags = r._flags;
|
||||
if (r._isRegex()) {
|
||||
// We have adopted it
|
||||
r._flags = _toFlags(Type::None, None);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool AsyncURIMatcher::matches(AsyncWebServerRequest *request) const {
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
if (_isRegex()) {
|
||||
// when type == Type::Regex, or when _value was auto-detected as regex
|
||||
std::smatch matches;
|
||||
std::string s(request->url().c_str());
|
||||
if (std::regex_search(s, matches, *pattern)) {
|
||||
for (size_t i = 1; i < matches.size(); ++i) {
|
||||
request->_pathParams.emplace_back(matches[i].str().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// extract matcher type from _flags
|
||||
Type type;
|
||||
uint16_t modifiers;
|
||||
std::tie(type, modifiers) = _fromFlags(_flags);
|
||||
|
||||
// apply modifiers
|
||||
String path = request->url();
|
||||
if (modifiers & CaseInsensitive) {
|
||||
path.toLowerCase();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case Type::All: return true;
|
||||
case Type::None: return false;
|
||||
case Type::Exact: return (_value == path);
|
||||
case Type::Prefix: return path.startsWith(_value);
|
||||
case Type::Extension:
|
||||
{
|
||||
int split = _value.lastIndexOf("/*.");
|
||||
return (split >= 0 && path.startsWith(_value.substring(0, split)) && path.endsWith(_value.substring(split + 2)));
|
||||
}
|
||||
case Type::BackwardCompatible: return (_value == path) || path.startsWith(_value + "/");
|
||||
default:
|
||||
// Should never happen - programming error
|
||||
assert("Invalid type");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
390
src/literals.h
390
src/literals.h
@@ -1,201 +1,221 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#pragma once
|
||||
|
||||
// Include WString.h for F() macro support on Arduino platforms
|
||||
#ifdef ARDUINO
|
||||
#include <WString.h>
|
||||
#endif
|
||||
|
||||
// Platform-specific string storage and return type
|
||||
#ifdef ARDUINO_ARCH_ESP8266
|
||||
// On ESP8266, use PROGMEM storage and return __FlashStringHelper*
|
||||
#include <pgmspace.h>
|
||||
#define DECLARE_STR(name, value) static const char name##_PROGMEM[] PROGMEM = value
|
||||
#define STR(name) (reinterpret_cast<const __FlashStringHelper *>(name##_PROGMEM))
|
||||
#define STR_RETURN_TYPE const __FlashStringHelper *
|
||||
#else
|
||||
// On other platforms, use regular constexpr for compile-time optimization
|
||||
#define DECLARE_STR(name, value) static constexpr const char *name = value
|
||||
#define STR(name) name
|
||||
#define STR_RETURN_TYPE const char *
|
||||
#endif
|
||||
|
||||
namespace asyncsrv {
|
||||
|
||||
static constexpr const char *empty = "";
|
||||
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_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_BODY = "body";
|
||||
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_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_event_ = "event: ";
|
||||
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";
|
||||
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_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_LOGIN_REQ = "Login Required";
|
||||
static constexpr const char *T_MULTIPART_ = "multipart/";
|
||||
static constexpr const char *T_name = "name";
|
||||
static constexpr const char *T_nc = "nc";
|
||||
static constexpr const char *T_no_cache = "no-cache";
|
||||
static constexpr const char *T_nonce = "nonce";
|
||||
static constexpr const char *T_none = "none";
|
||||
static constexpr const char *T_opaque = "opaque";
|
||||
static constexpr const char *T_qop = "qop";
|
||||
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_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_TRUE = "true";
|
||||
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__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_attachment[] = "attachment; filename=\"";
|
||||
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_BODY[] = "body";
|
||||
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_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_event_[] = "event: ";
|
||||
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";
|
||||
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_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_LOGIN_REQ[] = "Login Required";
|
||||
static constexpr const char T_MULTIPART_[] = "multipart/";
|
||||
static constexpr const char T_name[] = "name";
|
||||
static constexpr const char T_nc[] = "nc";
|
||||
static constexpr const char T_no_cache[] = "no-cache";
|
||||
static constexpr const char T_nonce[] = "nonce";
|
||||
static constexpr const char T_none[] = "none";
|
||||
static constexpr const char T_opaque[] = "opaque";
|
||||
static constexpr const char T_qop[] = "qop";
|
||||
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_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_TRUE[] = "true";
|
||||
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";
|
||||
|
||||
// HTTP Methods
|
||||
static constexpr const char *T_ANY = "ANY";
|
||||
static constexpr const char *T_GET = "GET";
|
||||
static constexpr const char *T_POST = "POST";
|
||||
static constexpr const char *T_PUT = "PUT";
|
||||
static constexpr const char *T_DELETE = "DELETE";
|
||||
static constexpr const char *T_PATCH = "PATCH";
|
||||
static constexpr const char *T_HEAD = "HEAD";
|
||||
static constexpr const char *T_OPTIONS = "OPTIONS";
|
||||
static constexpr const char *T_UNKNOWN = "UNKNOWN";
|
||||
static constexpr const char T_ANY[] = "ANY";
|
||||
static constexpr const char T_GET[] = "GET";
|
||||
static constexpr const char T_POST[] = "POST";
|
||||
static constexpr const char T_PUT[] = "PUT";
|
||||
static constexpr const char T_DELETE[] = "DELETE";
|
||||
static constexpr const char T_PATCH[] = "PATCH";
|
||||
static constexpr const char T_HEAD[] = "HEAD";
|
||||
static constexpr const char T_OPTIONS[] = "OPTIONS";
|
||||
static constexpr const char T_UNKNOWN[] = "UNKNOWN";
|
||||
|
||||
// Req content types
|
||||
static constexpr const char *T_RCT_NOT_USED = "RCT_NOT_USED";
|
||||
static constexpr const char *T_RCT_DEFAULT = "RCT_DEFAULT";
|
||||
static constexpr const char *T_RCT_HTTP = "RCT_HTTP";
|
||||
static constexpr const char *T_RCT_WS = "RCT_WS";
|
||||
static constexpr const char *T_RCT_EVENT = "RCT_EVENT";
|
||||
static constexpr const char *T_ERROR = "ERROR";
|
||||
static constexpr const char T_RCT_NOT_USED[] = "RCT_NOT_USED";
|
||||
static constexpr const char T_RCT_DEFAULT[] = "RCT_DEFAULT";
|
||||
static constexpr const char T_RCT_HTTP[] = "RCT_HTTP";
|
||||
static constexpr const char T_RCT_WS[] = "RCT_WS";
|
||||
static constexpr const char T_RCT_EVENT[] = "RCT_EVENT";
|
||||
static constexpr const char T_ERROR[] = "ERROR";
|
||||
|
||||
// extensions & MIME-Types
|
||||
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_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";
|
||||
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__mjs[] = ".mjs"; // MJS: JavaScript module format
|
||||
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_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";
|
||||
static constexpr const char *T_HTTP_CODE_101 = "Switching Protocols";
|
||||
static constexpr const char *T_HTTP_CODE_200 = "OK";
|
||||
static constexpr const char *T_HTTP_CODE_201 = "Created";
|
||||
static constexpr const char *T_HTTP_CODE_202 = "Accepted";
|
||||
static constexpr const char *T_HTTP_CODE_203 = "Non-Authoritative Information";
|
||||
static constexpr const char *T_HTTP_CODE_204 = "No Content";
|
||||
static constexpr const char *T_HTTP_CODE_205 = "Reset Content";
|
||||
static constexpr const char *T_HTTP_CODE_206 = "Partial Content";
|
||||
static constexpr const char *T_HTTP_CODE_300 = "Multiple Choices";
|
||||
static constexpr const char *T_HTTP_CODE_301 = "Moved Permanently";
|
||||
static constexpr const char *T_HTTP_CODE_302 = "Found";
|
||||
static constexpr const char *T_HTTP_CODE_303 = "See Other";
|
||||
static constexpr const char *T_HTTP_CODE_304 = "Not Modified";
|
||||
static constexpr const char *T_HTTP_CODE_305 = "Use Proxy";
|
||||
static constexpr const char *T_HTTP_CODE_307 = "Temporary Redirect";
|
||||
static constexpr const char *T_HTTP_CODE_400 = "Bad Request";
|
||||
static constexpr const char *T_HTTP_CODE_401 = "Unauthorized";
|
||||
static constexpr const char *T_HTTP_CODE_402 = "Payment Required";
|
||||
static constexpr const char *T_HTTP_CODE_403 = "Forbidden";
|
||||
static constexpr const char *T_HTTP_CODE_404 = "Not Found";
|
||||
static constexpr const char *T_HTTP_CODE_405 = "Method Not Allowed";
|
||||
static constexpr const char *T_HTTP_CODE_406 = "Not Acceptable";
|
||||
static constexpr const char *T_HTTP_CODE_407 = "Proxy Authentication Required";
|
||||
static constexpr const char *T_HTTP_CODE_408 = "Request Time-out";
|
||||
static constexpr const char *T_HTTP_CODE_409 = "Conflict";
|
||||
static constexpr const char *T_HTTP_CODE_410 = "Gone";
|
||||
static constexpr const char *T_HTTP_CODE_411 = "Length Required";
|
||||
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_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";
|
||||
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_ANY = "Unknown code";
|
||||
// Response codes - using DECLARE_STR macro for platform-specific storage
|
||||
DECLARE_STR(T_HTTP_CODE_100, "Continue");
|
||||
DECLARE_STR(T_HTTP_CODE_101, "Switching Protocols");
|
||||
DECLARE_STR(T_HTTP_CODE_200, "OK");
|
||||
DECLARE_STR(T_HTTP_CODE_201, "Created");
|
||||
DECLARE_STR(T_HTTP_CODE_202, "Accepted");
|
||||
DECLARE_STR(T_HTTP_CODE_203, "Non-Authoritative Information");
|
||||
DECLARE_STR(T_HTTP_CODE_204, "No Content");
|
||||
DECLARE_STR(T_HTTP_CODE_205, "Reset Content");
|
||||
DECLARE_STR(T_HTTP_CODE_206, "Partial Content");
|
||||
DECLARE_STR(T_HTTP_CODE_300, "Multiple Choices");
|
||||
DECLARE_STR(T_HTTP_CODE_301, "Moved Permanently");
|
||||
DECLARE_STR(T_HTTP_CODE_302, "Found");
|
||||
DECLARE_STR(T_HTTP_CODE_303, "See Other");
|
||||
DECLARE_STR(T_HTTP_CODE_304, "Not Modified");
|
||||
DECLARE_STR(T_HTTP_CODE_305, "Use Proxy");
|
||||
DECLARE_STR(T_HTTP_CODE_307, "Temporary Redirect");
|
||||
DECLARE_STR(T_HTTP_CODE_400, "Bad Request");
|
||||
DECLARE_STR(T_HTTP_CODE_401, "Unauthorized");
|
||||
DECLARE_STR(T_HTTP_CODE_402, "Payment Required");
|
||||
DECLARE_STR(T_HTTP_CODE_403, "Forbidden");
|
||||
DECLARE_STR(T_HTTP_CODE_404, "Not Found");
|
||||
DECLARE_STR(T_HTTP_CODE_405, "Method Not Allowed");
|
||||
DECLARE_STR(T_HTTP_CODE_406, "Not Acceptable");
|
||||
DECLARE_STR(T_HTTP_CODE_407, "Proxy Authentication Required");
|
||||
DECLARE_STR(T_HTTP_CODE_408, "Request Time-out");
|
||||
DECLARE_STR(T_HTTP_CODE_409, "Conflict");
|
||||
DECLARE_STR(T_HTTP_CODE_410, "Gone");
|
||||
DECLARE_STR(T_HTTP_CODE_411, "Length Required");
|
||||
DECLARE_STR(T_HTTP_CODE_412, "Precondition Failed");
|
||||
DECLARE_STR(T_HTTP_CODE_413, "Request Entity Too Large");
|
||||
DECLARE_STR(T_HTTP_CODE_414, "Request-URI Too Large");
|
||||
DECLARE_STR(T_HTTP_CODE_415, "Unsupported Media Type");
|
||||
DECLARE_STR(T_HTTP_CODE_416, "Requested Range Not Satisfiable");
|
||||
DECLARE_STR(T_HTTP_CODE_417, "Expectation Failed");
|
||||
DECLARE_STR(T_HTTP_CODE_429, "Too Many Requests");
|
||||
DECLARE_STR(T_HTTP_CODE_500, "Internal Server Error");
|
||||
DECLARE_STR(T_HTTP_CODE_501, "Not Implemented");
|
||||
DECLARE_STR(T_HTTP_CODE_502, "Bad Gateway");
|
||||
DECLARE_STR(T_HTTP_CODE_503, "Service Unavailable");
|
||||
DECLARE_STR(T_HTTP_CODE_504, "Gateway Time-out");
|
||||
DECLARE_STR(T_HTTP_CODE_505, "HTTP Version Not Supported");
|
||||
DECLARE_STR(T_HTTP_CODE_ANY, "Unknown code");
|
||||
|
||||
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,
|
||||
@@ -203,5 +223,5 @@ static constexpr const char *T_only_once_headers[] = {
|
||||
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]);
|
||||
|
||||
static constexpr size_t T__GZ_LEN = sizeof(T__gz) - 1;
|
||||
} // namespace asyncsrv
|
||||
|
||||
Reference in New Issue
Block a user