From 2333497adc44b8c8352003fc4d31650fd78b3569 Mon Sep 17 00:00:00 2001 From: Pablo2048 Date: Tue, 1 Oct 2024 13:38:03 +0200 Subject: [PATCH] Aktualizace na verzi 3.3.7 --- README.md | 1870 +++++++++++++++++++++- docs/index.md | 1870 +++++++++++++++++++++- examples/CaptivePortal/CaptivePortal.ino | 1 - examples/Filters/Filters.ino | 1 - examples/SimpleServer/SimpleServer.ino | 231 ++- library.json_ | 2 +- library.properties | 2 +- platformio.ini | 108 +- src/AsyncEventSource.cpp | 17 +- src/AsyncEventSource.h | 4 +- src/AsyncJson.cpp | 155 ++ src/AsyncJson.h | 234 +-- src/AsyncMessagePack.cpp | 106 ++ src/AsyncMessagePack.h | 147 +- src/AsyncWebHeader.cpp | 22 + src/AsyncWebSocket.cpp | 12 +- src/AsyncWebSocket.h | 17 +- src/ChunkPrint.cpp | 16 + src/ChunkPrint.h | 19 +- src/ESPAsyncWebServer.h | 404 +++-- src/Middleware.cpp | 253 +++ src/WebAuthentication.cpp | 66 +- src/WebAuthentication.h | 5 +- src/WebHandlerImpl.h | 75 +- src/WebHandlers.cpp | 91 +- src/WebRequest.cpp | 170 +- src/WebResponses.cpp | 7 +- src/WebServer.cpp | 40 +- src/literals.h | 10 +- 29 files changed, 5128 insertions(+), 827 deletions(-) create mode 100644 src/AsyncJson.cpp create mode 100644 src/AsyncMessagePack.cpp create mode 100644 src/AsyncWebHeader.cpp create mode 100644 src/ChunkPrint.cpp create mode 100644 src/Middleware.cpp diff --git a/README.md b/README.md index 70b2143..55efb4b 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,22 @@ Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static Fi This fork is based on [yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) and includes all the concurrency fixes. -## Coordinate and dependencies: +- [Coordinate and dependencies](#coordinate-and-dependencies) +- [Changes in this fork](#changes-in-this-fork) +- [Important recommendations](#important-recommendations) +- [`AsyncWebSocketMessageBuffer` and `makeBuffer()`](#asyncwebsocketmessagebuffer-and-makebuffer) +- [How to replace a response](#how-to-replace-a-response) +- [How to use Middleware](#how-to-use-middleware) +- [How to use authentication with AuthenticationMiddleware](#how-to-use-authentication-with-authenticationmiddleware) +- [Migration to Middleware to improve performance and memory usage](#migration-to-middleware-to-improve-performance-and-memory-usage) +- [Original Documentation](#original-documentation) + +## Coordinate and dependencies **WARNING** The library name was changed from `ESP Async WebServer` to `ESPAsyncWebServer` as per the Arduino Lint recommendations. ``` -mathieucarbou/ESPAsyncWebServer @ 3.2.4 +mathieucarbou/ESPAsyncWebServer @ 3.3.7 ``` Dependency: @@ -31,41 +41,56 @@ Dependency: ## Changes in this fork -- [@ayushsharma82](https://github.com/ayushsharma82) and [@mathieucarbou](https://github.com/mathieucarbou): Add RP2040 support ([#31](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/31)) -- [@mathieucarbou](https://github.com/mathieucarbou): `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client -- [@mathieucarbou](https://github.com/mathieucarbou): `write()` function public in `AsyncEventSource.h` -- [@mathieucarbou](https://github.com/mathieucarbou): `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setAuthentication(const String& username, const String& password)` -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full -- [@mathieucarbou](https://github.com/mathieucarbou): Added `StreamConcat` example to show how to stream multiple files in one response -- [@mathieucarbou](https://github.com/mathieucarbou): Added all flavors of `binary()`, `text()`, `binaryAll()` and `textAll()` in `AsyncWebSocket` -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino 3 / ESP-IDF 5.1 compatibility -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino Json 7 compatibility and backward compatible with 6 and 6 (changes in `AsyncJson.h`). The API to use Json has not changed. These are only internal changes. -- [@mathieucarbou](https://github.com/mathieucarbou): CI -- [@mathieucarbou](https://github.com/mathieucarbou): Depends on `mathieucarbou/AsyncTCP @ 3.2.5` -- [@mathieucarbou](https://github.com/mathieucarbou): Deployed in PlatformIO registry and Arduino IDE library manager -- [@mathieucarbou](https://github.com/mathieucarbou): Firmware size optimization: remove mbedtls dependency (accounts for 33KB in firmware) -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_WS_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): MessagePack Support ([#62](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/62)) -- [@mathieucarbou](https://github.com/mathieucarbou): Remove filename after inline in Content-Disposition header according to RFC2183 -- [@mathieucarbou](https://github.com/mathieucarbou): Removed SPIFFSEditor to reduce library size and maintainance. SPIFF si also deprecated. If you need it, please copy the files from the original repository in your project. This fork focus on maintaining the server part and the SPIFFEditor is an application which has nothing to do inside a server library. -- [@mathieucarbou](https://github.com/mathieucarbou): Resurrected `AsyncWebSocketMessageBuffer` and `makeBuffer()` in order to make the fork API-compatible with the original library from me-no-dev regarding WebSocket. -- [@mathieucarbou](https://github.com/mathieucarbou): Some code cleanup -- [@mathieucarbou](https://github.com/mathieucarbou): Use `-D DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients -- [@nilo85](https://github.com/nilo85): Add support for Auth & GET requests in AsyncCallbackJsonWebHandler ([#14](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/14)) -- [@p0p-x](https://github.com/p0p-x): ESP IDF Compatibility (added back CMakeLists.txt) ([#32](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/32)) -- [@tueddy](https://github.com/tueddy): Compile with Arduino 3 (ESP-IDF 5.1) ([#13](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/13)) -- [@vortigont](https://github.com/vortigont): Set real "Last-Modified" header based on file's LastWrite time ([#5](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/5)) -- [@vortigont](https://github.com/vortigont): Some websocket code cleanup ([#29](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/29)) -- [@vortigont](https://github.com/vortigont): Refactor code - replace DYI structs with STL objects ([#39](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/39)) +- (bug) A lot of bug fixes +- (ci) Better CI with a complete matrix of Arduino versions and boards +- (ci) Deployed in PlatformIO registry and Arduino IDE library manager +- (feat) **Arduino 3 / ESP-IDF 5** compatibility +- (feat) **ArduinoJson 7** compatibility +- (feat) **ESP32 / ESP8266 / RP2040** support +- (feat) **MessagePack** support +- (feat) **Middleware** support with pre-built middlewares for authentication, authorization, rate limiting, logging, cors, etc. +- (feat) **Request attributes** to store data on the request object +- (feat) **Response header** control and override +- (feat) **Response override**: support the ability to replace a previously sent response by another one +- (feat) **Resumable download** support using HEAD and bytes range +- (feat) `StreamConcat` example to show how to stream multiple files in one response +- (feat) Removed ESPIDF Editor (this is not the role of a web server library to do that - get the source files from the original repos if required) +- (perf) `char*` overloads to avoid using `String` +- (perf) `DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients +- (perf) `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full +- (perf) `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client +- (perf) `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client +- (perf) Code size improvements +- (perf) Lot of code cleanup and optimizations +- (perf) Performance improvements in terms of memory, speed and size -## Documentation +## Important recommendations -Usage and API stays the same as the original library. -Please look at the original libraries for more examples and documentation. +Most of the crashes are caused by improper configuration of the library for the project. +Here are some recommendations to avoid them. -- [https://github.com/me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) (original library) -- [https://github.com/yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) (fork of the original library) +1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` +2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. + The default value of `16384` might be too much for your project. + You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. +3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. + Default is `10`. +4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. + Default is `64`. +5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. + Default is `5000`. + +I personally use the following configuration in my projects because my WS messages can be big (up to 4k). +If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. + +```c++ + -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 + -D CONFIG_ASYNC_TCP_PRIORITY=10 + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 + -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 + -D WS_MAX_QUEUED_MESSAGES=64 +``` ## `AsyncWebSocketMessageBuffer` and `makeBuffer()` @@ -102,30 +127,1759 @@ void send(JsonDocument& doc) { I recommend to use the official API `AsyncWebSocketMessageBuffer` to retain further compatibility. -## Important recommendations - -Most of the crashes are caused by improper configuration of the library for the project. -Here are some recommendations to avoid them. - -1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` -2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. - The default value of `16384` might be too much for your project. - You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. -3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. - Default is `10`. -4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. - Default is `64`. -5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. - Default is `5000`. - -I personally use the following configuration in my projects because my WS messages can be big (up to 4k). -If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. +## How to replace a response ```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 + // It is possible to replace a response. + // The previous one will be deleted. + // Response sending happens when the handler returns. + server.on("/replace", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world"); + // oups! finally we want to send a different response + request->send(400, "text/plain", "validation error"); + }); ``` + +This will send error 400 instead of 200. + +## How to use Middleware + +Middleware is a way to intercept requests to perform some operations on them, like authentication, authorization, logging, etc and also act on the response headers. + +Middleware can either be attached to individual handlers, attached at the server level (thus applied to all handlers), or both. +They will be executed in the order they are attached, and they can stop the request processing by sending a response themselves. + +You can have a look at the [SimpleServer.ino](https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/examples/SimpleServer/SimpleServer.ino) example for some use cases. + +For example, such middleware would handle authentication and set some attributes on the request to make them available for the next middleware and for the handler which will process the request. + +```c++ +AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlewareNext next) { + if (!request->authenticate("user", "password")) { + return request->requestAuthentication(); + } + + request->setAttribute("user", "Mathieu"); + request->setAttribute("role", "staff"); + + next(); // continue processing + + // you can act one the response object + request->getResponse()->addHeader("X-Rate-Limit", "200"); +}); +``` + +**Here are the list of available middlewares:** + +- `AsyncMiddlewareFunction`: can convert a lambda function (`ArMiddlewareCallback`) to a middleware +- `AuthenticationMiddleware`: to handle basic/digest authentication globally or per handler +- `AuthorizationMiddleware`: to handle authorization globally or per handler +- `CorsMiddleware`: to handle CORS preflight request globally or per handler +- `HeaderFilterMiddleware`: to filter out headers from the request +- `HeaderFreeMiddleware`: to only keep some headers from the request, and remove the others +- `LoggerMiddleware`: to log requests globally or per handler with the same pattern as curl. Will also record request processing time +- `RateLimitMiddleware`: to limit the number of requests on a windows of time globally or per handler + +## How to use authentication with AuthenticationMiddleware + +Do not use the `setUsername()` and `setPassword()` methods on the hanlders anymore. +They are deprecated. +These methods were causing a copy of the username and password for each handler, which is not efficient. + +Now, you can use the `AuthenticationMiddleware` to handle authentication globally or per handler. + +```c++ +AuthenticationMiddleware authMiddleware; + +// [...] + +authMiddleware.setAuthType(AsyncAuthType::AUTH_DIGEST); +authMiddleware.setRealm("My app name"); +authMiddleware.setUsername("admin"); +authMiddleware.setPassword("admin"); +authMiddleware.setAuthFailureMessage("Authentication failed"); +authMiddleware.generateHash(); // optimization to avoid generating the hash at each request + +// [...] + +server.addMiddleware(&authMiddleware); // globally add authentication to the server + +// [...] + +myHandler.addMiddleware(&authMiddleware); // add authentication to a specific handler +``` + +## Migration to Middleware to improve performance and memory usage + +- `AsyncEventSource.authorizeConnect(...)` => do not use this method anymore: add a common `AuthorizationMiddleware` to the handler or server, and make sure to add it AFTER the `AuthenticationMiddleware` if you use authentication. +- `AsyncWebHandler.setAuthentication(...)` => do not use this method anymore: add a common `AuthenticationMiddleware` to the handler or server +- `ArUploadHandlerFunction` and `ArBodyHandlerFunction` => these callbacks receiving body data and upload and not calling anymore the authentication code for performance reasons. + These callbacks can be called multiple times during request parsing, so this is up to the user to now call the `AuthenticationMiddleware.allowed(request)` if needed and ideally when the method is called for the first time. + These callbacks are also not triggering the whole middleware chain since they are not part of the request processing workflow (they are not the final handler). + +## Original Documentation + +- [Why should you care](#why-should-you-care) +- [Important things to remember](#important-things-to-remember) +- [Principles of operation](#principles-of-operation) + - [The Async Web server](#the-async-web-server) + - [Request Life Cycle](#request-life-cycle) + - [Rewrites and how do they work](#rewrites-and-how-do-they-work) + - [Handlers and how do they work](#handlers-and-how-do-they-work) + - [Responses and how do they work](#responses-and-how-do-they-work) + - [Template processing](#template-processing) +- [Libraries and projects that use AsyncWebServer](#libraries-and-projects-that-use-asyncwebserver) +- [Request Variables](#request-variables) + - [Common Variables](#common-variables) + - [Headers](#headers) + - [GET, POST and FILE parameters](#get-post-and-file-parameters) + - [FILE Upload handling](#file-upload-handling) + - [Body data handling](#body-data-handling) + - [JSON body handling with ArduinoJson](#json-body-handling-with-arduinojson) +- [Responses](#responses) + - [Redirect to another URL](#redirect-to-another-url) + - [Basic response with HTTP Code](#basic-response-with-http-code) + - [Basic response with HTTP Code and extra headers](#basic-response-with-http-code-and-extra-headers) + - [Basic response with string content](#basic-response-with-string-content) + - [Basic response with string content and extra headers](#basic-response-with-string-content-and-extra-headers) + - [Send large webpage from PROGMEM](#send-large-webpage-from-progmem) + - [Send large webpage from PROGMEM and extra headers](#send-large-webpage-from-progmem-and-extra-headers) + - [Send large webpage from PROGMEM containing templates](#send-large-webpage-from-progmem-containing-templates) + - [Send large webpage from PROGMEM containing templates and extra headers](#send-large-webpage-from-progmem-containing-templates-and-extra-headers) + - [Send binary content from PROGMEM](#send-binary-content-from-progmem) + - [Respond with content coming from a Stream](#respond-with-content-coming-from-a-stream) + - [Respond with content coming from a Stream and extra headers](#respond-with-content-coming-from-a-stream-and-extra-headers) + - [Respond with content coming from a Stream containing templates](#respond-with-content-coming-from-a-stream-containing-templates) + - [Respond with content coming from a Stream containing templates and extra headers](#respond-with-content-coming-from-a-stream-containing-templates-and-extra-headers) + - [Respond with content coming from a File](#respond-with-content-coming-from-a-file) + - [Respond with content coming from a File and extra headers](#respond-with-content-coming-from-a-file-and-extra-headers) + - [Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates) + - [Respond with content using a callback](#respond-with-content-using-a-callback) + - [Respond with content using a callback and extra headers](#respond-with-content-using-a-callback-and-extra-headers) + - [Respond with file content using a callback and extra headers](#respond-with-file-content-using-a-callback-and-extra-headers) + - [Respond with content using a callback containing templates](#respond-with-content-using-a-callback-containing-templates) + - [Respond with content using a callback containing templates and extra headers](#respond-with-content-using-a-callback-containing-templates-and-extra-headers) + - [Chunked Response](#chunked-response) + - [Chunked Response containing templates](#chunked-response-containing-templates) + - [Print to response](#print-to-response) + - [ArduinoJson Basic Response](#arduinojson-basic-response) + - [ArduinoJson Advanced Response](#arduinojson-advanced-response) +- [Serving static files](#serving-static-files) + - [Serving specific file by name](#serving-specific-file-by-name) + - [Serving files in directory](#serving-files-in-directory) + - [Serving static files with authentication](#serving-static-files-with-authentication) + - [Specifying Cache-Control header](#specifying-cache-control-header) + - [Specifying Date-Modified header](#specifying-date-modified-header) + - [Specifying Template Processor callback](#specifying-template-processor-callback) + - [Serving static files by custom handling](#serving-static-files-by-custom-handling) +- [Param Rewrite With Matching](#param-rewrite-with-matching) +- [Using filters](#using-filters) + - [Serve different site files in AP mode](#serve-different-site-files-in-ap-mode) + - [Rewrite to different index on AP](#rewrite-to-different-index-on-ap) + - [Serving different hosts](#serving-different-hosts) + - [Determine interface inside callbacks](#determine-interface-inside-callbacks) +- [Bad Responses](#bad-responses) + - [Respond with content using a callback without content length to HTTP/1.0 clients](#respond-with-content-using-a-callback-without-content-length-to-http10-clients) +- [Async WebSocket Plugin](#async-websocket-plugin) + - [Async WebSocket Event](#async-websocket-event) + - [Methods for sending data to a socket client](#methods-for-sending-data-to-a-socket-client) + - [Direct access to web socket message buffer](#direct-access-to-web-socket-message-buffer) + - [Limiting the number of web socket clients](#limiting-the-number-of-web-socket-clients) +- [Async Event Source Plugin](#async-event-source-plugin) + - [Setup Event Source on the server](#setup-event-source-on-the-server) + - [Setup Event Source in the browser](#setup-event-source-in-the-browser) +- [Scanning for available WiFi Networks](#scanning-for-available-wifi-networks) +- [Remove handlers and rewrites](#remove-handlers-and-rewrites) +- [Setting up the server](#setting-up-the-server) + - [Setup global and class functions as request handlers](#setup-global-and-class-functions-as-request-handlers) + - [Methods for controlling websocket connections](#methods-for-controlling-websocket-connections) + - [Adding Default Headers](#adding-default-headers) + - [Path variable](#path-variable) + +### Why should you care + +- Using asynchronous network means that you can handle more than one connection at the same time +- You are called once the request is ready and parsed +- When you send the response, you are immediately ready to handle other connections + while the server is taking care of sending the response in the background +- Speed is OMG +- Easy to use API, HTTP Basic and Digest MD5 Authentication (default), ChunkedResponse +- Easily extendible to handle any type of content +- Supports Continue 100 +- Async WebSocket plugin offering different locations without extra servers or ports +- Async EventSource (Server-Sent Events) plugin to send events to the browser +- URL Rewrite plugin for conditional and permanent url rewrites +- ServeStatic plugin that supports cache, Last-Modified, default index and more +- Simple template processing engine to handle templates + +### Important things to remember + +- This is fully asynchronous server and as such does not run on the loop thread. +- You can not use yield or delay or any function that uses them inside the callbacks +- The server is smart enough to know when to close the connection and free resources +- You can not send more than one response to a single request + +### Principles of operation + +#### The Async Web server + +- Listens for connections +- Wraps the new clients into `Request` +- Keeps track of clients and cleans memory +- Manages `Rewrites` and apply them on the request url +- Manages `Handlers` and attaches them to Requests + +#### Request Life Cycle + +- TCP connection is received by the server +- The connection is wrapped inside `Request` object +- When the request head is received (type, url, get params, http version and host), + the server goes through all `Rewrites` (in the order they were added) to rewrite the url and inject query parameters, + next, it goes through all attached `Handlers`(in the order they were added) trying to find one + that `canHandle` the given request. If none are found, the default(catch-all) handler is attached. +- The rest of the request is received, calling the `handleUpload` or `handleBody` methods of the `Handler` if they are needed (POST+File/Body) +- When the whole request is parsed, the result is given to the `handleRequest` method of the `Handler` and is ready to be responded to +- In the `handleRequest` method, to the `Request` is attached a `Response` object (see below) that will serve the response data back to the client +- When the `Response` is sent, the client is closed and freed from the memory + +#### Rewrites and how do they work + +- The `Rewrites` are used to rewrite the request url and/or inject get parameters for a specific request url path. +- All `Rewrites` are evaluated on the request in the order they have been added to the server. +- The `Rewrite` will change the request url only if the request url (excluding get parameters) is fully match + the rewrite url, and when the optional `Filter` callback return true. +- Setting a `Filter` to the `Rewrite` enables to control when to apply the rewrite, decision can be based on + request url, http version, request host/port/target host, get parameters or the request client's localIP or remoteIP. +- Two filter callbacks are provided: `ON_AP_FILTER` to execute the rewrite when request is made to the AP interface, + `ON_STA_FILTER` to execute the rewrite when request is made to the STA interface. +- The `Rewrite` can specify a target url with optional get parameters, e.g. `/to-url?with=params` + +#### Handlers and how do they work + +- The `Handlers` are used for executing specific actions to particular requests +- One `Handler` instance can be attached to any request and lives together with the server +- Setting a `Filter` to the `Handler` enables to control when to apply the handler, decision can be based on + request url, http version, request host/port/target host, get parameters or the request client's localIP or remoteIP. +- Two filter callbacks are provided: `ON_AP_FILTER` to execute the rewrite when request is made to the AP interface, + `ON_STA_FILTER` to execute the rewrite when request is made to the STA interface. +- The `canHandle` method is used for handler specific control on whether the requests can be handled + and for declaring any interesting headers that the `Request` should parse. Decision can be based on request + method, request url, http version, request host/port/target host and get parameters +- Once a `Handler` is attached to given `Request` (`canHandle` returned true) + that `Handler` takes care to receive any file/data upload and attach a `Response` + once the `Request` has been fully parsed +- `Handlers` are evaluated in the order they are attached to the server. The `canHandle` is called only + if the `Filter` that was set to the `Handler` return true. +- The first `Handler` that can handle the request is selected, not further `Filter` and `canHandle` are called. + +#### Responses and how do they work + +- The `Response` objects are used to send the response data back to the client +- The `Response` object lives with the `Request` and is freed on end or disconnect +- Different techniques are used depending on the response type to send the data in packets + returning back almost immediately and sending the next packet when this one is received. + Any time in between is spent to run the user loop and handle other network packets +- Responding asynchronously is probably the most difficult thing for most to understand +- Many different options exist for the user to make responding a background task + +#### Template processing + +- ESPAsyncWebserver contains simple template processing engine. +- Template processing can be added to most response types. +- Currently it supports only replacing template placeholders with actual values. No conditional processing, cycles, etc. +- Placeholders are delimited with `%` symbols. Like this: `%TEMPLATE_PLACEHOLDER%`. +- It works by extracting placeholder name from response text and passing it to user provided function which should return actual value to be used instead of placeholder. +- Since it's user provided function, it is possible for library users to implement conditional processing and cycles themselves. +- Since it's impossible to know the actual response size after template processing step in advance (and, therefore, to include it in response headers), the response becomes [chunked](#chunked-response). + +### Libraries and projects that use AsyncWebServer + +- [WebSocketToSerial](https://github.com/hallard/WebSocketToSerial) - Debug serial devices through the web browser +- [Sattrack](https://github.com/Hopperpop/Sattrack) - Track the ISS with ESP8266 +- [ESP Radio](https://github.com/Edzelf/Esp-radio) - Icecast radio based on ESP8266 and VS1053 +- [VZero](https://github.com/andig/vzero) - the Wireless zero-config controller for volkszaehler.org +- [ESPurna](https://bitbucket.org/xoseperez/espurna) - ESPurna ("spark" in Catalan) is a custom C firmware for ESP8266 based smart switches. It was originally developed with the ITead Sonoff in mind. +- [fauxmoESP](https://bitbucket.org/xoseperez/fauxmoesp) - Belkin WeMo emulator library for ESP8266. +- [ESP-RFID](https://github.com/omersiar/esp-rfid) - MFRC522 RFID Access Control Management project for ESP8266. + +### Request Variables + +#### Common Variables + +```cpp +request->version(); // uint8_t: 0 = HTTP/1.0, 1 = HTTP/1.1 +request->method(); // enum: HTTP_GET, HTTP_POST, HTTP_DELETE, HTTP_PUT, HTTP_PATCH, HTTP_HEAD, HTTP_OPTIONS +request->url(); // String: URL of the request (not including host, port or GET parameters) +request->host(); // String: The requested host (can be used for virtual hosting) +request->contentType(); // String: ContentType of the request (not avaiable in Handler::canHandle) +request->contentLength(); // size_t: ContentLength of the request (not avaiable in Handler::canHandle) +request->multipart(); // bool: True if the request has content type "multipart" +``` + +#### Headers + +```cpp +//List all collected headers +int headers = request->headers(); +int i; +for(i=0;igetHeader(i); + Serial.printf("HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str()); +} + +//get specific header by name +if(request->hasHeader("MyHeader")){ + AsyncWebHeader* h = request->getHeader("MyHeader"); + Serial.printf("MyHeader: %s\n", h->value().c_str()); +} + +//List all collected headers (Compatibility) +int headers = request->headers(); +int i; +for(i=0;iheaderName(i).c_str(), request->header(i).c_str()); +} + +//get specific header by name (Compatibility) +if(request->hasHeader("MyHeader")){ + Serial.printf("MyHeader: %s\n", request->header("MyHeader").c_str()); +} +``` + +#### GET, POST and FILE parameters + +```cpp +//List all parameters +int params = request->params(); +for(int i=0;igetParam(i); + if(p->isFile()){ //p->isPost() is also true + Serial.printf("FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size()); + } else if(p->isPost()){ + Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } else { + Serial.printf("GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } +} + +//Check if GET parameter exists +if(request->hasParam("download")) + AsyncWebParameter* p = request->getParam("download"); + +//Check if POST (but not File) parameter exists +if(request->hasParam("download", true)) + AsyncWebParameter* p = request->getParam("download", true); + +//Check if FILE was uploaded +if(request->hasParam("download", true, true)) + AsyncWebParameter* p = request->getParam("download", true, true); + +//List all parameters (Compatibility) +int args = request->args(); +for(int i=0;iargName(i).c_str(), request->arg(i).c_str()); +} + +//Check if parameter exists (Compatibility) +if(request->hasArg("download")) + String arg = request->arg("download"); +``` + +#### FILE Upload handling + +```cpp +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + Serial.printf("UploadStart: %s\n", filename.c_str()); + } + for(size_t i=0; i(); + // ... +}); +server.addHandler(handler); +``` + +### Responses + +#### Redirect to another URL + +```cpp +//to local url +request->redirect("/login"); + +//to external url +request->redirect("http://esp8266.com"); +``` + +#### Basic response with HTTP Code + +```cpp +request->send(404); //Sends 404 File Not Found +``` + +#### Basic response with HTTP Code and extra headers + +```cpp +AsyncWebServerResponse *response = request->beginResponse(404); //Sends 404 File Not Found +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Basic response with string content + +```cpp +request->send(200, "text/plain", "Hello World!"); +``` + +#### Basic response with string content and extra headers + +```cpp +AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Hello World!"); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send large webpage from PROGMEM + +```cpp +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +request->send_P(200, "text/html", index_html); +``` + +#### Send large webpage from PROGMEM and extra headers + +```cpp +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send large webpage from PROGMEM containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +request->send_P(200, "text/html", index_html, processor); +``` + +#### Send large webpage from PROGMEM containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send binary content from PROGMEM + +```cpp + +//File: favicon.ico.gz, Size: 726 +#define favicon_ico_gz_len 726 +const uint8_t favicon_ico_gz[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x0B, 0x87, 0x90, 0x57, 0x00, 0x03, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6F, + 0x6E, 0x2E, 0x69, 0x63, 0x6F, 0x00, 0xCD, 0x53, 0x5F, 0x48, 0x9A, 0x51, 0x14, 0xBF, 0x62, 0x6D, + 0x86, 0x96, 0xA9, 0x64, 0xD3, 0xFE, 0xA8, 0x99, 0x65, 0x1A, 0xB4, 0x8A, 0xA8, 0x51, 0x54, 0x23, + 0xA8, 0x11, 0x49, 0x51, 0x8A, 0x34, 0x62, 0x93, 0x85, 0x31, 0x58, 0x44, 0x12, 0x45, 0x2D, 0x58, + 0xF5, 0x52, 0x41, 0x10, 0x23, 0x82, 0xA0, 0x20, 0x98, 0x2F, 0xC1, 0x26, 0xED, 0xA1, 0x20, 0x89, + 0x04, 0xD7, 0x83, 0x58, 0x20, 0x28, 0x04, 0xAB, 0xD1, 0x9B, 0x8C, 0xE5, 0xC3, 0x60, 0x32, 0x64, + 0x0E, 0x56, 0xBF, 0x9D, 0xEF, 0xF6, 0x30, 0x82, 0xED, 0xAD, 0x87, 0xDD, 0x8F, 0xF3, 0xDD, 0x8F, + 0x73, 0xCF, 0xEF, 0x9C, 0xDF, 0x39, 0xBF, 0xFB, 0x31, 0x26, 0xA2, 0x27, 0x37, 0x97, 0xD1, 0x5B, + 0xCF, 0x9E, 0x67, 0x30, 0xA6, 0x66, 0x8C, 0x99, 0xC9, 0xC8, 0x45, 0x9E, 0x6B, 0x3F, 0x5F, 0x74, + 0xA6, 0x94, 0x5E, 0xDB, 0xFF, 0xB2, 0xE6, 0xE7, 0xE7, 0xF9, 0xDE, 0xD6, 0xD6, 0x96, 0xDB, 0xD8, + 0xD8, 0x78, 0xBF, 0xA1, 0xA1, 0xC1, 0xDA, 0xDC, 0xDC, 0x2C, 0xEB, 0xED, 0xED, 0x15, 0x9B, 0xCD, + 0xE6, 0x4A, 0x83, 0xC1, 0xE0, 0x2E, 0x29, 0x29, 0x99, 0xD6, 0x6A, 0xB5, 0x4F, 0x75, 0x3A, 0x9D, + 0x61, 0x75, 0x75, 0x95, 0xB5, 0xB7, 0xB7, 0xDF, 0xC8, 0xD1, 0xD4, 0xD4, 0xF4, 0xB0, 0xBA, 0xBA, + 0xFA, 0x83, 0xD5, 0x6A, 0xFD, 0x5A, 0x5E, 0x5E, 0x9E, 0x28, 0x2D, 0x2D, 0x0D, 0x10, 0xC6, 0x4B, + 0x98, 0x78, 0x5E, 0x5E, 0xDE, 0x95, 0x42, 0xA1, 0x40, 0x4E, 0x4E, 0xCE, 0x65, 0x76, 0x76, 0xF6, + 0x47, 0xB5, 0x5A, 0x6D, 0x4F, 0x26, 0x93, 0xA2, 0xD6, 0xD6, 0x56, 0x8E, 0x6D, 0x69, 0x69, 0xD1, + 0x11, 0x36, 0x62, 0xB1, 0x58, 0x60, 0x32, 0x99, 0xA0, 0xD7, 0xEB, 0x51, 0x58, 0x58, 0x88, 0xFC, + 0xFC, 0x7C, 0x10, 0x16, 0x02, 0x56, 0x2E, 0x97, 0x43, 0x2A, 0x95, 0x42, 0x2C, 0x16, 0x23, 0x33, + 0x33, 0x33, 0xAE, 0x52, 0xA9, 0x1E, 0x64, 0x65, 0x65, 0x71, 0x7C, 0x7D, 0x7D, 0xBD, 0x93, 0xEA, + 0xFE, 0x30, 0x1A, 0x8D, 0xE8, 0xEC, 0xEC, 0xC4, 0xE2, 0xE2, 0x22, 0x6A, 0x6A, 0x6A, 0x40, 0x39, + 0x41, 0xB5, 0x38, 0x4E, 0xC8, 0x33, 0x3C, 0x3C, 0x0C, 0x87, 0xC3, 0xC1, 0x6B, 0x54, 0x54, 0x54, + 0xBC, 0xE9, 0xEB, 0xEB, 0x93, 0x5F, 0x5C, 0x5C, 0x30, 0x8A, 0x9D, 0x2E, 0x2B, 0x2B, 0xBB, 0xA2, + 0x3E, 0x41, 0xBD, 0x21, 0x1E, 0x8F, 0x63, 0x6A, 0x6A, 0x0A, 0x81, 0x40, 0x00, 0x94, 0x1B, 0x3D, + 0x3D, 0x3D, 0x42, 0x3C, 0x96, 0x96, 0x96, 0x70, 0x7E, 0x7E, 0x8E, 0xE3, 0xE3, 0x63, 0xF8, 0xFD, + 0xFE, 0xB4, 0xD7, 0xEB, 0xF5, 0x8F, 0x8F, 0x8F, 0x5B, 0x68, 0x5E, 0x6F, 0x05, 0xCE, 0xB4, 0xE3, + 0xE8, 0xE8, 0x08, 0x27, 0x27, 0x27, 0xD8, 0xDF, 0xDF, 0xC7, 0xD9, 0xD9, 0x19, 0x6C, 0x36, 0x1B, + 0x36, 0x36, 0x36, 0x38, 0x9F, 0x85, 0x85, 0x05, 0xAC, 0xAF, 0xAF, 0x23, 0x1A, 0x8D, 0x22, 0x91, + 0x48, 0x20, 0x16, 0x8B, 0xFD, 0xDA, 0xDA, 0xDA, 0x7A, 0x41, 0x33, 0x7E, 0x57, 0x50, 0x50, 0x80, + 0x89, 0x89, 0x09, 0x84, 0xC3, 0x61, 0x6C, 0x6F, 0x6F, 0x23, 0x12, 0x89, 0xE0, 0xE0, 0xE0, 0x00, + 0x43, 0x43, 0x43, 0x58, 0x5E, 0x5E, 0xE6, 0x9C, 0x7D, 0x3E, 0x1F, 0x46, 0x47, 0x47, 0x79, 0xBE, + 0xBD, 0xBD, 0x3D, 0xE1, 0x3C, 0x1D, 0x0C, 0x06, 0x9F, 0x10, 0xB7, 0xC7, 0x84, 0x4F, 0xF6, 0xF7, + 0xF7, 0x63, 0x60, 0x60, 0x00, 0x83, 0x83, 0x83, 0x18, 0x19, 0x19, 0xC1, 0xDC, 0xDC, 0x1C, 0x8F, + 0x17, 0x7C, 0xA4, 0x27, 0xE7, 0x34, 0x39, 0x39, 0x89, 0x9D, 0x9D, 0x1D, 0x6E, 0x54, 0xE3, 0x13, + 0xE5, 0x34, 0x11, 0x37, 0x49, 0x51, 0x51, 0xD1, 0x4B, 0xA5, 0x52, 0xF9, 0x45, 0x26, 0x93, 0x5D, + 0x0A, 0xF3, 0x92, 0x48, 0x24, 0xA0, 0x6F, 0x14, 0x17, 0x17, 0xA3, 0xB6, 0xB6, 0x16, 0x5D, 0x5D, + 0x5D, 0x7C, 0x1E, 0xBB, 0xBB, 0xBB, 0x9C, 0xD7, 0xE1, 0xE1, 0x21, 0x42, 0xA1, 0xD0, 0x6B, 0xD2, + 0x45, 0x4C, 0x33, 0x12, 0x34, 0xCC, 0xA0, 0x19, 0x54, 0x92, 0x56, 0x0E, 0xD2, 0xD9, 0x43, 0xF8, + 0xCF, 0x82, 0x56, 0xC2, 0xDC, 0xEB, 0xEA, 0xEA, 0x38, 0x7E, 0x6C, 0x6C, 0x4C, 0xE0, 0xFE, 0x9D, + 0xB8, 0xBF, 0xA7, 0xFA, 0xAF, 0x56, 0x56, 0x56, 0xEE, 0x6D, 0x6E, 0x6E, 0xDE, 0xB8, 0x47, 0x55, + 0x55, 0x55, 0x6C, 0x66, 0x66, 0x46, 0x44, 0xDA, 0x3B, 0x34, 0x1A, 0x4D, 0x94, 0xB0, 0x3F, 0x09, + 0x7B, 0x45, 0xBD, 0xA5, 0x5D, 0x2E, 0x57, 0x8C, 0x7A, 0x73, 0xD9, 0xED, 0xF6, 0x3B, 0x84, 0xFF, + 0xE7, 0x7D, 0xA6, 0x3A, 0x2C, 0x95, 0x4A, 0xB1, 0x8E, 0x8E, 0x0E, 0x6D, 0x77, 0x77, 0xB7, 0xCD, + 0xE9, 0x74, 0x3E, 0x73, 0xBB, 0xDD, 0x8F, 0x3C, 0x1E, 0x8F, 0xE6, 0xF4, 0xF4, 0x94, 0xAD, 0xAD, + 0xAD, 0xDD, 0xDE, 0xCF, 0x73, 0x0B, 0x0B, 0xB8, 0xB6, 0xE0, 0x5D, 0xC6, 0x66, 0xC5, 0xE4, 0x10, + 0x4C, 0xF4, 0xF7, 0xD8, 0x59, 0xF2, 0x7F, 0xA3, 0xB8, 0xB4, 0xFC, 0x0F, 0xEE, 0x37, 0x70, 0xEC, + 0x16, 0x4A, 0x7E, 0x04, 0x00, 0x00 +}; + +AsyncWebServerResponse *response = request->beginResponse_P(200, "image/x-icon", favicon_ico_gz, favicon_ico_gz_len); +response->addHeader("Content-Encoding", "gzip"); +request->send(response); +``` + +#### Respond with content coming from a Stream + +```cpp +//read 12 bytes from Serial and send them as Content Type text/plain +request->send(Serial, "text/plain", 12); +``` + +#### Respond with content coming from a Stream and extra headers + +```cpp +//read 12 bytes from Serial and send them as Content Type text/plain +AsyncWebServerResponse *response = request->beginResponse(Serial, "text/plain", 12); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a Stream containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//read 12 bytes from Serial and send them as Content Type text/plain +request->send(Serial, "text/plain", 12, processor); +``` + +#### Respond with content coming from a Stream containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//read 12 bytes from Serial and send them as Content Type text/plain +AsyncWebServerResponse *response = request->beginResponse(Serial, "text/plain", 12, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a File + +```cpp +//Send index.htm with default content type +request->send(SPIFFS, "/index.htm"); + +//Send index.htm as text +request->send(SPIFFS, "/index.htm", "text/plain"); + +//Download index.htm +request->send(SPIFFS, "/index.htm", String(), true); +``` + +#### Respond with content coming from a File and extra headers + +```cpp +//Send index.htm with default content type +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm"); + +//Send index.htm as text +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm", "text/plain"); + +//Download index.htm +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm", String(), true); + +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a File containing templates + +Internally uses [Chunked Response](#chunked-response). + +Index.htm contents: + +``` +%HELLO_FROM_TEMPLATE% +``` + +Somewhere in source files: + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//Send index.htm with template processor function +request->send(SPIFFS, "/index.htm", String(), false, processor); +``` + +#### Respond with content using a callback + +```cpp +//send 128 bytes as plain text +request->send("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}); +``` + +#### Respond with content using a callback and extra headers + +```cpp +//send 128 bytes as plain text +AsyncWebServerResponse *response = request->beginResponse("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with file content using a callback and extra headers + +With this code your ESP is able to serve even large (large in terms of ESP, e.g. 100kB) files +without memory problems. + +You need to create a file handler in outer function (to have a single one for request) but use +it in a lambda. The catch is that the lambda has it's own lifecycle which may/will cause it's +called after the original function is over thus the original file handle is destroyed. Using the +captured `&file` in the lambda then causes segfault (Hello, Exception 9!) and the whole ESP crashes. +By using this code, you tell the compiler to move the handle into the lambda so it won't be +destroyed when outer function (that one where you call `request->send(response)`) ends. + +```cpp +const File file = ... // e.g. SPIFFS.open(path, "r"); + +const contentType = "application/javascript"; + +AsyncWebServerResponse *response = request->beginResponse( + contentType, + file.size(), + [file](uint8_t *buffer, size_t maxLen, size_t total) mutable -> size_t { + int bytes = file.read(buffer, maxLen); + + // close file at the end + if (bytes + total == file.size()) file.close(); + + return max(0, bytes); // return 0 even when no bytes were loaded + } +); + +if (gzipped) { + response->addHeader(F("Content-Encoding"), F("gzip")); +} + +request->send(response); +``` + +#### Respond with content using a callback containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//send 128 bytes as plain text +request->send("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}, processor); +``` + +#### Respond with content using a callback containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//send 128 bytes as plain text +AsyncWebServerResponse *response = request->beginResponse("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Chunked Response + +Used when content length is unknown. Works best if the client supports HTTP/1.1 + +```cpp +AsyncWebServerResponse *response = request->beginChunkedResponse("text/plain", [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Chunked Response containing templates + +Used when content length is unknown. Works best if the client supports HTTP/1.1 + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +AsyncWebServerResponse *response = request->beginChunkedResponse("text/plain", [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Print to response + +```cpp +AsyncResponseStream *response = request->beginResponseStream("text/html"); +response->addHeader("Server","ESP Async Web Server"); +response->printf("Webpage at %s", request->url().c_str()); + +response->print("

Hello "); +response->print(request->client()->remoteIP()); +response->print("

"); + +response->print("

General

"); +response->print("
    "); +response->printf("
  • Version: HTTP/1.%u
  • ", request->version()); +response->printf("
  • Method: %s
  • ", request->methodToString()); +response->printf("
  • URL: %s
  • ", request->url().c_str()); +response->printf("
  • Host: %s
  • ", request->host().c_str()); +response->printf("
  • ContentType: %s
  • ", request->contentType().c_str()); +response->printf("
  • ContentLength: %u
  • ", request->contentLength()); +response->printf("
  • Multipart: %s
  • ", request->multipart()?"true":"false"); +response->print("
"); + +response->print("

Headers

"); +response->print("
    "); +int headers = request->headers(); +for(int i=0;igetHeader(i); + response->printf("
  • %s: %s
  • ", h->name().c_str(), h->value().c_str()); +} +response->print("
"); + +response->print("

Parameters

"); +response->print("
    "); +int params = request->params(); +for(int i=0;igetParam(i); + if(p->isFile()){ + response->printf("
  • FILE[%s]: %s, size: %u
  • ", p->name().c_str(), p->value().c_str(), p->size()); + } else if(p->isPost()){ + response->printf("
  • POST[%s]: %s
  • ", p->name().c_str(), p->value().c_str()); + } else { + response->printf("
  • GET[%s]: %s
  • ", p->name().c_str(), p->value().c_str()); + } +} +response->print("
"); + +response->print(""); +//send the response last +request->send(response); +``` + +#### ArduinoJson Basic Response + +This way of sending Json is great for when the result is below 4KB + +```cpp +#include "AsyncJson.h" +#include "ArduinoJson.h" + + +AsyncResponseStream *response = request->beginResponseStream("application/json"); +DynamicJsonBuffer jsonBuffer; +JsonObject &root = jsonBuffer.createObject(); +root["heap"] = ESP.getFreeHeap(); +root["ssid"] = WiFi.SSID(); +root.printTo(*response); +request->send(response); +``` + +#### ArduinoJson Advanced Response + +This response can handle really large Json objects (tested to 40KB) +There isn't any noticeable speed decrease for small results with the method above +Since ArduinoJson does not allow reading parts of the string, the whole Json has to +be passed every time a chunks needs to be sent, which shows speed decrease proportional +to the resulting json packets + +```cpp +#include "AsyncJson.h" +#include "ArduinoJson.h" + + +AsyncJsonResponse * response = new AsyncJsonResponse(); +response->addHeader("Server","ESP Async Web Server"); +JsonObject& root = response->getRoot(); +root["heap"] = ESP.getFreeHeap(); +root["ssid"] = WiFi.SSID(); +response->setLength(); +request->send(response); +``` + +### Serving static files + +In addition to serving files from SPIFFS as described above, the server provide a dedicated handler that optimize the +performance of serving files from SPIFFS - `AsyncStaticWebHandler`. Use `server.serveStatic()` function to +initialize and add a new instance of `AsyncStaticWebHandler` to the server. +The Handler will not handle the request if the file does not exists, e.g. the server will continue to look for another +handler that can handle the request. +Notice that you can chain setter functions to setup the handler, or keep a pointer to change it at a later time. + +#### Serving specific file by name + +```cpp +// Serve the file "/www/page.htm" when request url is "/page.htm" +server.serveStatic("/page.htm", SPIFFS, "/www/page.htm"); +``` + +#### Serving files in directory + +To serve files in a directory, the path to the files should specify a directory in SPIFFS and ends with "/". + +```cpp +// Serve files in directory "/www/" when request url starts with "/" +// Request to the root or none existing files will try to server the defualt +// file name "index.htm" if exists +server.serveStatic("/", SPIFFS, "/www/"); + +// Server with different default file +server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("default.html"); +``` + +#### Serving static files with authentication + +```cpp +server + .serveStatic("/", SPIFFS, "/www/") + .setDefaultFile("default.html") + .setAuthentication("user", "pass"); +``` + +#### Specifying Cache-Control header + +It is possible to specify Cache-Control header value to reduce the number of calls to the server once the client loaded +the files. For more information on Cache-Control values see [Cache-Control](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) + +```cpp +// Cache responses for 10 minutes (600 seconds) +server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=600"); + +//*** Change Cache-Control after server setup *** + +// During setup - keep a pointer to the handler +AsyncStaticWebHandler* handler = &server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=600"); + +// At a later event - change Cache-Control +handler->setCacheControl("max-age=30"); +``` + +#### Specifying Date-Modified header + +It is possible to specify Date-Modified header to enable the server to return Not-Modified (304) response for requests +with "If-Modified-Since" header with the same value, instead of responding with the actual file content. + +```cpp +// Update the date modified string every time files are updated +server.serveStatic("/", SPIFFS, "/www/").setLastModified("Mon, 20 Jun 2016 14:00:00 GMT"); + +//*** Chage last modified value at a later stage *** + +// During setup - read last modified value from config or EEPROM +String date_modified = loadDateModified(); +AsyncStaticWebHandler* handler = &server.serveStatic("/", SPIFFS, "/www/"); +handler->setLastModified(date_modified); + +// At a later event when files are updated +String date_modified = getNewDateModfied(); +saveDateModified(date_modified); // Save for next reset +handler->setLastModified(date_modified); +``` + +#### Specifying Template Processor callback + +It is possible to specify template processor for static files. For information on template processor see +[Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates). + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +server.serveStatic("/", SPIFFS, "/www/").setTemplateProcessor(processor); +``` + +#### Serving static files by custom handling + +It may happen your static files are too big and the ESP will crash the request before it sends the whole file. +In that case, you can handle static files with custom file serving through not found handler. + +This code below is more-or-less equivalent to this: + +```cpp +webServer.serveStatic("/", SPIFFS, STATIC_FILES_PREFIX).setDefaultFile("index.html") +``` + +First, declare the handling function: + +```cpp +bool handleStaticFile(AsyncWebServerRequest *request) { + String path = STATIC_FILES_PREFIX + request->url(); + + if (path.endsWith("/")) path += F("index.html"); + + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + + if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + bool gzipped = false; + if (SPIFFS.exists(pathWithGz)) { + gzipped = true; + path += ".gz"; + } + + // TODO serve the file + + return true; + } + + return false; +} +``` + +And then configure your webserver: + +```cpp +webServer.onNotFound([](AsyncWebServerRequest *request) { + if (handleStaticFile(request)) return; + + request->send(404); +}); +``` + +You may want to try [Respond with file content using a callback and extra headers](#respond-with-file-content-using-a-callback-and-extra-headers) +For actual serving the file. + +### Param Rewrite With Matching + +It is possible to rewrite the request url with parameter matchg. Here is an example with one parameter: +Rewrite for example "/radio/{frequence}" -> "/radio?f={frequence}" + +```cpp +class OneParamRewrite : public AsyncWebRewrite +{ + protected: + String _urlPrefix; + int _paramIndex; + String _paramsBackup; + + public: + OneParamRewrite(const char* from, const char* to) + : AsyncWebRewrite(from, to) { + + _paramIndex = _from.indexOf('{'); + + if( _paramIndex >=0 && _from.endsWith("}")) { + _urlPrefix = _from.substring(0, _paramIndex); + int index = _params.indexOf('{'); + if(index >= 0) { + _params = _params.substring(0, index); + } + } else { + _urlPrefix = _from; + } + _paramsBackup = _params; + } + + bool match(AsyncWebServerRequest *request) override { + if(request->url().startsWith(_urlPrefix)) { + if(_paramIndex >= 0) { + _params = _paramsBackup + request->url().substring(_paramIndex); + } else { + _params = _paramsBackup; + } + return true; + + } else { + return false; + } + } +}; +``` + +Usage: + +```cpp + server.addRewrite( new OneParamRewrite("/radio/{frequence}", "/radio?f={frequence}") ); +``` + +### Using filters + +Filters can be set to `Rewrite` or `Handler` in order to control when to apply the rewrite and consider the handler. +A filter is a callback function that evaluates the request and return a boolean `true` to include the item +or `false` to exclude it. +Two filter callback are provided for convince: + +- `ON_STA_FILTER` - return true when requests are made to the STA (station mode) interface. +- `ON_AP_FILTER` - return true when requests are made to the AP (access point) interface. + +#### Serve different site files in AP mode + +```cpp +server.serveStatic("/", SPIFFS, "/www/").setFilter(ON_STA_FILTER); +server.serveStatic("/", SPIFFS, "/ap/").setFilter(ON_AP_FILTER); +``` + +#### Rewrite to different index on AP + +```cpp +// Serve the file "/www/index-ap.htm" in AP, and the file "/www/index.htm" on STA +server.rewrite("/", "index.htm"); +server.rewrite("/index.htm", "index-ap.htm").setFilter(ON_AP_FILTER); +server.serveStatic("/", SPIFFS, "/www/"); +``` + +#### Serving different hosts + +```cpp +// Filter callback using request host +bool filterOnHost1(AsyncWebServerRequest *request) { return request->host() == "host1"; } + +// Server setup: server files in "/host1/" to requests for "host1", and files in "/www/" otherwise. +server.serveStatic("/", SPIFFS, "/host1/").setFilter(filterOnHost1); +server.serveStatic("/", SPIFFS, "/www/"); +``` + +#### Determine interface inside callbacks + +```cpp + String RedirectUrl = "http://"; + if (ON_STA_FILTER(request)) { + RedirectUrl += WiFi.localIP().toString(); + } else { + RedirectUrl += WiFi.softAPIP().toString(); + } + RedirectUrl += "/index.htm"; + request->redirect(RedirectUrl); +``` + +### Bad Responses + +Some responses are implemented, but you should not use them, because they do not conform to HTTP. +The following example will lead to unclean close of the connection and more time wasted +than providing the length of the content + +#### Respond with content using a callback without content length to HTTP/1.0 clients + +```cpp +//This is used as fallback for chunked responses to HTTP/1.0 Clients +request->send("text/plain", 0, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}); +``` + +### Async WebSocket Plugin + +The server includes a web socket plugin which lets you define different WebSocket locations to connect to +without starting another listening service or using different port + +#### Async WebSocket Event + +```cpp + +void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + if(type == WS_EVT_CONNECT){ + //client connected + os_printf("ws[%s][%u] connect\n", server->url(), client->id()); + client->printf("Hello Client %u :)", client->id()); + client->ping(); + } else if(type == WS_EVT_DISCONNECT){ + //client disconnected + os_printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); + } else if(type == WS_EVT_ERROR){ + //error was received from the other end + os_printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); + } else if(type == WS_EVT_PONG){ + //pong message was received (in response to a ping request maybe) + os_printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); + } else if(type == WS_EVT_DATA){ + //data packet + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if(info->final && info->index == 0 && info->len == len){ + //the whole message is in a single frame and we got all of it's data + os_printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); + if(info->opcode == WS_TEXT){ + data[len] = 0; + os_printf("%s\n", (char*)data); + } else { + for(size_t i=0; i < info->len; i++){ + os_printf("%02x ", data[i]); + } + os_printf("\n"); + } + if(info->opcode == WS_TEXT) + client->text("I got your text message"); + else + client->binary("I got your binary message"); + } else { + //message is comprised of multiple frames or the frame is split into multiple packets + if(info->index == 0){ + if(info->num == 0) + os_printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + os_printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); + } + + os_printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); + if(info->message_opcode == WS_TEXT){ + data[len] = 0; + os_printf("%s\n", (char*)data); + } else { + for(size_t i=0; i < len; i++){ + os_printf("%02x ", data[i]); + } + os_printf("\n"); + } + + if((info->index + len) == info->len){ + os_printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); + if(info->final){ + os_printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + if(info->message_opcode == WS_TEXT) + client->text("I got your text message"); + else + client->binary("I got your binary message"); + } + } + } + } +} +``` + +#### Methods for sending data to a socket client + +```cpp + + + +//Server methods +AsyncWebSocket ws("/ws"); +//printf to a client +ws.printf((uint32_t)client_id, arguments...); +//printf to all clients +ws.printfAll(arguments...); +//printf_P to a client +ws.printf_P((uint32_t)client_id, PSTR(format), arguments...); +//printfAll_P to all clients +ws.printfAll_P(PSTR(format), arguments...); +//send text to a client +ws.text((uint32_t)client_id, (char*)text); +ws.text((uint32_t)client_id, (uint8_t*)text, (size_t)len); +//send text from PROGMEM to a client +ws.text((uint32_t)client_id, PSTR("text")); +const char flash_text[] PROGMEM = "Text to send" +ws.text((uint32_t)client_id, FPSTR(flash_text)); +//send text to all clients +ws.textAll((char*)text); +ws.textAll((uint8_t*)text, (size_t)len); +//send binary to a client +ws.binary((uint32_t)client_id, (char*)binary); +ws.binary((uint32_t)client_id, (uint8_t*)binary, (size_t)len); +//send binary from PROGMEM to a client +const uint8_t flash_binary[] PROGMEM = { 0x01, 0x02, 0x03, 0x04 }; +ws.binary((uint32_t)client_id, flash_binary, 4); +//send binary to all clients +ws.binaryAll((char*)binary); +ws.binaryAll((uint8_t*)binary, (size_t)len); +//HTTP Authenticate before switch to Websocket protocol +ws.setAuthentication("user", "pass"); + +//client methods +AsyncWebSocketClient * client; +//printf +client->printf(arguments...); +//printf_P +client->printf_P(PSTR(format), arguments...); +//send text +client->text((char*)text); +client->text((uint8_t*)text, (size_t)len); +//send text from PROGMEM +client->text(PSTR("text")); +const char flash_text[] PROGMEM = "Text to send"; +client->text(FPSTR(flash_text)); +//send binary +client->binary((char*)binary); +client->binary((uint8_t*)binary, (size_t)len); +//send binary from PROGMEM +const uint8_t flash_binary[] PROGMEM = { 0x01, 0x02, 0x03, 0x04 }; +client->binary(flash_binary, 4); +``` + +#### Direct access to web socket message buffer + +When sending a web socket message using the above methods a buffer is created. Under certain circumstances you might want to manipulate or populate this buffer directly from your application, for example to prevent unnecessary duplications of the data. This example below shows how to create a buffer and print data to it from an ArduinoJson object then send it. + +```cpp +void sendDataWs(AsyncWebSocketClient * client) +{ + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + root["a"] = "abc"; + root["b"] = "abcd"; + root["c"] = "abcde"; + root["d"] = "abcdef"; + root["e"] = "abcdefg"; + size_t len = root.measureLength(); + AsyncWebSocketMessageBuffer * buffer = ws.makeBuffer(len); // creates a buffer (len + 1) for you. + if (buffer) { + root.printTo((char *)buffer->get(), len + 1); + if (client) { + client->text(buffer); + } else { + ws.textAll(buffer); + } + } +} +``` + +#### Limiting the number of web socket clients + +Browsers sometimes do not correctly close the websocket connection, even when the close() function is called in javascript. This will eventually exhaust the web server's resources and will cause the server to crash. Periodically calling the cleanClients() function from the main loop() function limits the number of clients by closing the oldest client when the maximum number of clients has been exceeded. This can called be every cycle, however, if you wish to use less power, then calling as infrequently as once per second is sufficient. + +```cpp +void loop(){ + ws.cleanupClients(); +} +``` + +### Async Event Source Plugin + +The server includes EventSource (Server-Sent Events) plugin which can be used to send short text events to the browser. +Difference between EventSource and WebSockets is that EventSource is single direction, text-only protocol. + +#### Setup Event Source on the server + +```cpp +AsyncWebServer server(80); +AsyncEventSource events("/events"); + +void setup(){ + // setup ...... + events.onConnect([](AsyncEventSourceClient *client){ + if(client->lastId()){ + Serial.printf("Client reconnected! Last message ID that it gat is: %u\n", client->lastId()); + } + //send event with message "hello!", id current millis + // and set reconnect delay to 1 second + client->send("hello!",NULL,millis(),1000); + }); + //HTTP Basic authentication + events.setAuthentication("user", "pass"); + server.addHandler(&events); + // setup ...... +} + +void loop(){ + if(eventTriggered){ // your logic here + //send event "myevent" + events.send("my event content","myevent",millis()); + } +} +``` + +#### Setup Event Source in the browser + +```javascript +if (!!window.EventSource) { + var source = new EventSource("/events"); + + source.addEventListener( + "open", + function (e) { + console.log("Events Connected"); + }, + false + ); + + source.addEventListener( + "error", + function (e) { + if (e.target.readyState != EventSource.OPEN) { + console.log("Events Disconnected"); + } + }, + false + ); + + source.addEventListener( + "message", + function (e) { + console.log("message", e.data); + }, + false + ); + + source.addEventListener( + "myevent", + function (e) { + console.log("myevent", e.data); + }, + false + ); +} +``` + +### Scanning for available WiFi Networks + +```cpp +//First request will return 0 results unless you start scan from somewhere else (loop/setup) +//Do not request more often than 3-5 seconds +server.on("/scan", HTTP_GET, [](AsyncWebServerRequest *request){ + String json = "["; + int n = WiFi.scanComplete(); + if(n == -2){ + WiFi.scanNetworks(true); + } else if(n){ + for (int i = 0; i < n; ++i){ + if(i) json += ","; + json += "{"; + json += "\"rssi\":"+String(WiFi.RSSI(i)); + json += ",\"ssid\":\""+WiFi.SSID(i)+"\""; + json += ",\"bssid\":\""+WiFi.BSSIDstr(i)+"\""; + json += ",\"channel\":"+String(WiFi.channel(i)); + json += ",\"secure\":"+String(WiFi.encryptionType(i)); + json += ",\"hidden\":"+String(WiFi.isHidden(i)?"true":"false"); + json += "}"; + } + WiFi.scanDelete(); + if(WiFi.scanComplete() == -2){ + WiFi.scanNetworks(true); + } + } + json += "]"; + request->send(200, "application/json", json); + json = String(); +}); +``` + +### Remove handlers and rewrites + +Server goes through handlers in same order as they were added. You can't simple add handler with same path to override them. +To remove handler: + +```arduino +// save callback for particular URL path +auto handler = server.on("/some/path", [](AsyncWebServerRequest *request){ + //do something useful +}); +// when you don't need handler anymore remove it +server.removeHandler(&handler); + +// same with rewrites +server.removeRewrite(&someRewrite); + +server.onNotFound([](AsyncWebServerRequest *request){ + request->send(404); +}); + +// remove server.onNotFound handler +server.onNotFound(NULL); + +// remove all rewrites, handlers and onNotFound/onFileUpload/onRequestBody callbacks +server.reset(); +``` + +### Setting up the server + +```cpp +#include "ESPAsyncTCP.h" +#include "ESPAsyncWebServer.h" + +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws +AsyncEventSource events("/events"); // event source (Server-Sent events) + +const char* ssid = "your-ssid"; +const char* password = "your-pass"; +const char* http_username = "admin"; +const char* http_password = "admin"; + +//flag to use from web update to reboot the ESP +bool shouldReboot = false; + +void onRequest(AsyncWebServerRequest *request){ + //Handle Unknown Request + request->send(404); +} + +void onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + //Handle body +} + +void onUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + //Handle upload +} + +void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + //Handle WebSocket event +} + +void setup(){ + Serial.begin(115200); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.printf("WiFi Failed!\n"); + return; + } + + // attach AsyncWebSocket + ws.onEvent(onEvent); + server.addHandler(&ws); + + // attach AsyncEventSource + server.addHandler(&events); + + // respond to GET requests on URL /heap + server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, "text/plain", String(ESP.getFreeHeap())); + }); + + // upload a file to /upload + server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ + request->send(200); + }, onUpload); + + // send a file when /index is requested + server.on("/index", HTTP_ANY, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/index.htm"); + }); + + // HTTP basic authentication + server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + request->send(200, "text/plain", "Login Success!"); + }); + + // Simple Firmware Update Form + server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, "text/html", "
"); + }); + server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request){ + shouldReboot = !Update.hasError(); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot?"OK":"FAIL"); + response->addHeader("Connection", "close"); + request->send(response); + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + Serial.printf("Update Start: %s\n", filename.c_str()); + Update.runAsync(true); + if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){ + Update.printError(Serial); + } + } + if(!Update.hasError()){ + if(Update.write(data, len) != len){ + Update.printError(Serial); + } + } + if(final){ + if(Update.end(true)){ + Serial.printf("Update Success: %uB\n", index+len); + } else { + Update.printError(Serial); + } + } + }); + + // attach filesystem root at URL /fs + server.serveStatic("/fs", SPIFFS, "/"); + + // Catch-All Handlers + // Any request that can not find a Handler that canHandle it + // ends in the callbacks below. + server.onNotFound(onRequest); + server.onFileUpload(onUpload); + server.onRequestBody(onBody); + + server.begin(); +} + +void loop(){ + if(shouldReboot){ + Serial.println("Rebooting..."); + delay(100); + ESP.restart(); + } + static char temp[128]; + sprintf(temp, "Seconds since boot: %u", millis()/1000); + events.send(temp, "time"); //send event "time" +} +``` + +#### Setup global and class functions as request handlers + +```cpp +#include +#include +#include +#include + +void handleRequest(AsyncWebServerRequest *request){} + +class WebClass { +public : + AsyncWebServer classWebServer = AsyncWebServer(81); + + WebClass(){}; + + void classRequest (AsyncWebServerRequest *request){} + + void begin(){ + // attach global request handler + classWebServer.on("/example", HTTP_ANY, handleRequest); + + // attach class request handler + classWebServer.on("/example", HTTP_ANY, std::bind(&WebClass::classRequest, this, std::placeholders::_1)); + } +}; + +AsyncWebServer globalWebServer(80); +WebClass webClassInstance; + +void setup() { + // attach global request handler + globalWebServer.on("/example", HTTP_ANY, handleRequest); + + // attach class request handler + globalWebServer.on("/example", HTTP_ANY, std::bind(&WebClass::classRequest, webClassInstance, std::placeholders::_1)); +} + +void loop() { + +} +``` + +#### Methods for controlling websocket connections + +```cpp + // Disable client connections if it was activated + if ( ws.enabled() ) + ws.enable(false); + + // enable client connections if it was disabled + if ( !ws.enabled() ) + ws.enable(true); +``` + +Example of OTA code + +```cpp + // OTA callbacks + ArduinoOTA.onStart([]() { + // Clean SPIFFS + SPIFFS.end(); + + // Disable client connections + ws.enable(false); + + // Advertise connected clients what's going on + ws.textAll("OTA Update Started"); + + // Close them + ws.closeAll(); + + }); + +``` + +#### Adding Default Headers + +In some cases, such as when working with CORS, or with some sort of custom authentication system, +you might need to define a header that should get added to all responses (including static, websocket and EventSource). +The DefaultHeaders singleton allows you to do this. + +Example: + +```cpp +DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); +webServer.begin(); +``` + +_NOTE_: You will still need to respond to the OPTIONS method for CORS pre-flight in most cases. (unless you are only using GET) + +This is one option: + +```cpp +webServer.onNotFound([](AsyncWebServerRequest *request) { + if (request->method() == HTTP_OPTIONS) { + request->send(200); + } else { + request->send(404); + } +}); +``` + +#### Path variable + +With path variable you can create a custom regex rule for a specific parameter in a route. +For example we want a `sensorId` parameter in a route rule to match only a integer. + +```cpp + server.on("^\\/sensor\\/([0-9]+)$", HTTP_GET, [] (AsyncWebServerRequest *request) { + String sensorId = request->pathArg(0); + }); +``` + +_NOTE_: All regex patterns starts with `^` and ends with `$` + +To enable the `Path variable` support, you have to define the buildflag `-DASYNCWEBSERVER_REGEX`. + +For Arduino IDE create/update `platform.local.txt`: + +`Windows`: C:\Users\(username)\AppData\Local\Arduino15\packages\\`{espxxxx}`\hardware\\`espxxxx`\\`{version}`\platform.local.txt + +`Linux`: ~/.arduino15/packages/`{espxxxx}`/hardware/`{espxxxx}`/`{version}`/platform.local.txt + +Add/Update the following line: + +``` + compiler.cpp.extra_flags=-DDASYNCWEBSERVER_REGEX +``` + +For platformio modify `platformio.ini`: + +```ini +[env:myboard] +build_flags = + -DASYNCWEBSERVER_REGEX +``` + +_NOTE_: By enabling `ASYNCWEBSERVER_REGEX`, `` will be included. This will add an 100k to your binary. diff --git a/docs/index.md b/docs/index.md index 70b2143..55efb4b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,12 +15,22 @@ Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static Fi This fork is based on [yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) and includes all the concurrency fixes. -## Coordinate and dependencies: +- [Coordinate and dependencies](#coordinate-and-dependencies) +- [Changes in this fork](#changes-in-this-fork) +- [Important recommendations](#important-recommendations) +- [`AsyncWebSocketMessageBuffer` and `makeBuffer()`](#asyncwebsocketmessagebuffer-and-makebuffer) +- [How to replace a response](#how-to-replace-a-response) +- [How to use Middleware](#how-to-use-middleware) +- [How to use authentication with AuthenticationMiddleware](#how-to-use-authentication-with-authenticationmiddleware) +- [Migration to Middleware to improve performance and memory usage](#migration-to-middleware-to-improve-performance-and-memory-usage) +- [Original Documentation](#original-documentation) + +## Coordinate and dependencies **WARNING** The library name was changed from `ESP Async WebServer` to `ESPAsyncWebServer` as per the Arduino Lint recommendations. ``` -mathieucarbou/ESPAsyncWebServer @ 3.2.4 +mathieucarbou/ESPAsyncWebServer @ 3.3.7 ``` Dependency: @@ -31,41 +41,56 @@ Dependency: ## Changes in this fork -- [@ayushsharma82](https://github.com/ayushsharma82) and [@mathieucarbou](https://github.com/mathieucarbou): Add RP2040 support ([#31](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/31)) -- [@mathieucarbou](https://github.com/mathieucarbou): `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client -- [@mathieucarbou](https://github.com/mathieucarbou): `write()` function public in `AsyncEventSource.h` -- [@mathieucarbou](https://github.com/mathieucarbou): `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setAuthentication(const String& username, const String& password)` -- [@mathieucarbou](https://github.com/mathieucarbou): Added `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full -- [@mathieucarbou](https://github.com/mathieucarbou): Added `StreamConcat` example to show how to stream multiple files in one response -- [@mathieucarbou](https://github.com/mathieucarbou): Added all flavors of `binary()`, `text()`, `binaryAll()` and `textAll()` in `AsyncWebSocket` -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino 3 / ESP-IDF 5.1 compatibility -- [@mathieucarbou](https://github.com/mathieucarbou): Arduino Json 7 compatibility and backward compatible with 6 and 6 (changes in `AsyncJson.h`). The API to use Json has not changed. These are only internal changes. -- [@mathieucarbou](https://github.com/mathieucarbou): CI -- [@mathieucarbou](https://github.com/mathieucarbou): Depends on `mathieucarbou/AsyncTCP @ 3.2.5` -- [@mathieucarbou](https://github.com/mathieucarbou): Deployed in PlatformIO registry and Arduino IDE library manager -- [@mathieucarbou](https://github.com/mathieucarbou): Firmware size optimization: remove mbedtls dependency (accounts for 33KB in firmware) -- [@mathieucarbou](https://github.com/mathieucarbou): Made DEFAULT_MAX_WS_CLIENTS customizable -- [@mathieucarbou](https://github.com/mathieucarbou): MessagePack Support ([#62](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/62)) -- [@mathieucarbou](https://github.com/mathieucarbou): Remove filename after inline in Content-Disposition header according to RFC2183 -- [@mathieucarbou](https://github.com/mathieucarbou): Removed SPIFFSEditor to reduce library size and maintainance. SPIFF si also deprecated. If you need it, please copy the files from the original repository in your project. This fork focus on maintaining the server part and the SPIFFEditor is an application which has nothing to do inside a server library. -- [@mathieucarbou](https://github.com/mathieucarbou): Resurrected `AsyncWebSocketMessageBuffer` and `makeBuffer()` in order to make the fork API-compatible with the original library from me-no-dev regarding WebSocket. -- [@mathieucarbou](https://github.com/mathieucarbou): Some code cleanup -- [@mathieucarbou](https://github.com/mathieucarbou): Use `-D DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients -- [@nilo85](https://github.com/nilo85): Add support for Auth & GET requests in AsyncCallbackJsonWebHandler ([#14](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/14)) -- [@p0p-x](https://github.com/p0p-x): ESP IDF Compatibility (added back CMakeLists.txt) ([#32](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/32)) -- [@tueddy](https://github.com/tueddy): Compile with Arduino 3 (ESP-IDF 5.1) ([#13](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/13)) -- [@vortigont](https://github.com/vortigont): Set real "Last-Modified" header based on file's LastWrite time ([#5](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/5)) -- [@vortigont](https://github.com/vortigont): Some websocket code cleanup ([#29](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/29)) -- [@vortigont](https://github.com/vortigont): Refactor code - replace DYI structs with STL objects ([#39](https://github.com/mathieucarbou/ESPAsyncWebServer/pull/39)) +- (bug) A lot of bug fixes +- (ci) Better CI with a complete matrix of Arduino versions and boards +- (ci) Deployed in PlatformIO registry and Arduino IDE library manager +- (feat) **Arduino 3 / ESP-IDF 5** compatibility +- (feat) **ArduinoJson 7** compatibility +- (feat) **ESP32 / ESP8266 / RP2040** support +- (feat) **MessagePack** support +- (feat) **Middleware** support with pre-built middlewares for authentication, authorization, rate limiting, logging, cors, etc. +- (feat) **Request attributes** to store data on the request object +- (feat) **Response header** control and override +- (feat) **Response override**: support the ability to replace a previously sent response by another one +- (feat) **Resumable download** support using HEAD and bytes range +- (feat) `StreamConcat` example to show how to stream multiple files in one response +- (feat) Removed ESPIDF Editor (this is not the role of a web server library to do that - get the source files from the original repos if required) +- (perf) `char*` overloads to avoid using `String` +- (perf) `DEFAULT_MAX_WS_CLIENTS` to change the number of allows WebSocket clients and use `cleanupClients()` to help cleanup resources about dead clients +- (perf) `setCloseClientOnQueueFull(bool)` which can be set on a client to either close the connection or discard messages but not close the connection when the queue is full +- (perf) `SSE_MAX_QUEUED_MESSAGES` to control the maximum number of messages that can be queued for a SSE client +- (perf) `WS_MAX_QUEUED_MESSAGES`: control the maximum number of messages that can be queued for a Websocket client +- (perf) Code size improvements +- (perf) Lot of code cleanup and optimizations +- (perf) Performance improvements in terms of memory, speed and size -## Documentation +## Important recommendations -Usage and API stays the same as the original library. -Please look at the original libraries for more examples and documentation. +Most of the crashes are caused by improper configuration of the library for the project. +Here are some recommendations to avoid them. -- [https://github.com/me-no-dev/ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) (original library) -- [https://github.com/yubox-node-org/ESPAsyncWebServer](https://github.com/yubox-node-org/ESPAsyncWebServer) (fork of the original library) +1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` +2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. + The default value of `16384` might be too much for your project. + You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. +3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. + Default is `10`. +4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. + Default is `64`. +5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. + Default is `5000`. + +I personally use the following configuration in my projects because my WS messages can be big (up to 4k). +If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. + +```c++ + -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 + -D CONFIG_ASYNC_TCP_PRIORITY=10 + -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 + -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 + -D WS_MAX_QUEUED_MESSAGES=64 +``` ## `AsyncWebSocketMessageBuffer` and `makeBuffer()` @@ -102,30 +127,1759 @@ void send(JsonDocument& doc) { I recommend to use the official API `AsyncWebSocketMessageBuffer` to retain further compatibility. -## Important recommendations - -Most of the crashes are caused by improper configuration of the library for the project. -Here are some recommendations to avoid them. - -1. Set the running core to be on the same core of your application (usually core 1) `-D CONFIG_ASYNC_TCP_RUNNING_CORE=1` -2. Set the stack size appropriately with `-D CONFIG_ASYNC_TCP_STACK_SIZE=16384`. - The default value of `16384` might be too much for your project. - You can look at the [MycilaTaskMonitor](https://oss.carbou.me/MycilaTaskMonitor) project to monitor the stack usage. -3. You can change **if you know what you are doing** the task priority with `-D CONFIG_ASYNC_TCP_PRIORITY=10`. - Default is `10`. -4. You can increase the queue size with `-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128`. - Default is `64`. -5. You can decrease the maximum ack time `-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000`. - Default is `5000`. - -I personally use the following configuration in my projects because my WS messages can be big (up to 4k). -If you have smaller messages, you can increase `WS_MAX_QUEUED_MESSAGES` to 128. +## How to replace a response ```c++ - -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 - -D CONFIG_ASYNC_TCP_PRIORITY=10 - -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 - -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 - -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 - -D WS_MAX_QUEUED_MESSAGES=64 + // It is possible to replace a response. + // The previous one will be deleted. + // Response sending happens when the handler returns. + server.on("/replace", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world"); + // oups! finally we want to send a different response + request->send(400, "text/plain", "validation error"); + }); ``` + +This will send error 400 instead of 200. + +## How to use Middleware + +Middleware is a way to intercept requests to perform some operations on them, like authentication, authorization, logging, etc and also act on the response headers. + +Middleware can either be attached to individual handlers, attached at the server level (thus applied to all handlers), or both. +They will be executed in the order they are attached, and they can stop the request processing by sending a response themselves. + +You can have a look at the [SimpleServer.ino](https://github.com/mathieucarbou/ESPAsyncWebServer/blob/main/examples/SimpleServer/SimpleServer.ino) example for some use cases. + +For example, such middleware would handle authentication and set some attributes on the request to make them available for the next middleware and for the handler which will process the request. + +```c++ +AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlewareNext next) { + if (!request->authenticate("user", "password")) { + return request->requestAuthentication(); + } + + request->setAttribute("user", "Mathieu"); + request->setAttribute("role", "staff"); + + next(); // continue processing + + // you can act one the response object + request->getResponse()->addHeader("X-Rate-Limit", "200"); +}); +``` + +**Here are the list of available middlewares:** + +- `AsyncMiddlewareFunction`: can convert a lambda function (`ArMiddlewareCallback`) to a middleware +- `AuthenticationMiddleware`: to handle basic/digest authentication globally or per handler +- `AuthorizationMiddleware`: to handle authorization globally or per handler +- `CorsMiddleware`: to handle CORS preflight request globally or per handler +- `HeaderFilterMiddleware`: to filter out headers from the request +- `HeaderFreeMiddleware`: to only keep some headers from the request, and remove the others +- `LoggerMiddleware`: to log requests globally or per handler with the same pattern as curl. Will also record request processing time +- `RateLimitMiddleware`: to limit the number of requests on a windows of time globally or per handler + +## How to use authentication with AuthenticationMiddleware + +Do not use the `setUsername()` and `setPassword()` methods on the hanlders anymore. +They are deprecated. +These methods were causing a copy of the username and password for each handler, which is not efficient. + +Now, you can use the `AuthenticationMiddleware` to handle authentication globally or per handler. + +```c++ +AuthenticationMiddleware authMiddleware; + +// [...] + +authMiddleware.setAuthType(AsyncAuthType::AUTH_DIGEST); +authMiddleware.setRealm("My app name"); +authMiddleware.setUsername("admin"); +authMiddleware.setPassword("admin"); +authMiddleware.setAuthFailureMessage("Authentication failed"); +authMiddleware.generateHash(); // optimization to avoid generating the hash at each request + +// [...] + +server.addMiddleware(&authMiddleware); // globally add authentication to the server + +// [...] + +myHandler.addMiddleware(&authMiddleware); // add authentication to a specific handler +``` + +## Migration to Middleware to improve performance and memory usage + +- `AsyncEventSource.authorizeConnect(...)` => do not use this method anymore: add a common `AuthorizationMiddleware` to the handler or server, and make sure to add it AFTER the `AuthenticationMiddleware` if you use authentication. +- `AsyncWebHandler.setAuthentication(...)` => do not use this method anymore: add a common `AuthenticationMiddleware` to the handler or server +- `ArUploadHandlerFunction` and `ArBodyHandlerFunction` => these callbacks receiving body data and upload and not calling anymore the authentication code for performance reasons. + These callbacks can be called multiple times during request parsing, so this is up to the user to now call the `AuthenticationMiddleware.allowed(request)` if needed and ideally when the method is called for the first time. + These callbacks are also not triggering the whole middleware chain since they are not part of the request processing workflow (they are not the final handler). + +## Original Documentation + +- [Why should you care](#why-should-you-care) +- [Important things to remember](#important-things-to-remember) +- [Principles of operation](#principles-of-operation) + - [The Async Web server](#the-async-web-server) + - [Request Life Cycle](#request-life-cycle) + - [Rewrites and how do they work](#rewrites-and-how-do-they-work) + - [Handlers and how do they work](#handlers-and-how-do-they-work) + - [Responses and how do they work](#responses-and-how-do-they-work) + - [Template processing](#template-processing) +- [Libraries and projects that use AsyncWebServer](#libraries-and-projects-that-use-asyncwebserver) +- [Request Variables](#request-variables) + - [Common Variables](#common-variables) + - [Headers](#headers) + - [GET, POST and FILE parameters](#get-post-and-file-parameters) + - [FILE Upload handling](#file-upload-handling) + - [Body data handling](#body-data-handling) + - [JSON body handling with ArduinoJson](#json-body-handling-with-arduinojson) +- [Responses](#responses) + - [Redirect to another URL](#redirect-to-another-url) + - [Basic response with HTTP Code](#basic-response-with-http-code) + - [Basic response with HTTP Code and extra headers](#basic-response-with-http-code-and-extra-headers) + - [Basic response with string content](#basic-response-with-string-content) + - [Basic response with string content and extra headers](#basic-response-with-string-content-and-extra-headers) + - [Send large webpage from PROGMEM](#send-large-webpage-from-progmem) + - [Send large webpage from PROGMEM and extra headers](#send-large-webpage-from-progmem-and-extra-headers) + - [Send large webpage from PROGMEM containing templates](#send-large-webpage-from-progmem-containing-templates) + - [Send large webpage from PROGMEM containing templates and extra headers](#send-large-webpage-from-progmem-containing-templates-and-extra-headers) + - [Send binary content from PROGMEM](#send-binary-content-from-progmem) + - [Respond with content coming from a Stream](#respond-with-content-coming-from-a-stream) + - [Respond with content coming from a Stream and extra headers](#respond-with-content-coming-from-a-stream-and-extra-headers) + - [Respond with content coming from a Stream containing templates](#respond-with-content-coming-from-a-stream-containing-templates) + - [Respond with content coming from a Stream containing templates and extra headers](#respond-with-content-coming-from-a-stream-containing-templates-and-extra-headers) + - [Respond with content coming from a File](#respond-with-content-coming-from-a-file) + - [Respond with content coming from a File and extra headers](#respond-with-content-coming-from-a-file-and-extra-headers) + - [Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates) + - [Respond with content using a callback](#respond-with-content-using-a-callback) + - [Respond with content using a callback and extra headers](#respond-with-content-using-a-callback-and-extra-headers) + - [Respond with file content using a callback and extra headers](#respond-with-file-content-using-a-callback-and-extra-headers) + - [Respond with content using a callback containing templates](#respond-with-content-using-a-callback-containing-templates) + - [Respond with content using a callback containing templates and extra headers](#respond-with-content-using-a-callback-containing-templates-and-extra-headers) + - [Chunked Response](#chunked-response) + - [Chunked Response containing templates](#chunked-response-containing-templates) + - [Print to response](#print-to-response) + - [ArduinoJson Basic Response](#arduinojson-basic-response) + - [ArduinoJson Advanced Response](#arduinojson-advanced-response) +- [Serving static files](#serving-static-files) + - [Serving specific file by name](#serving-specific-file-by-name) + - [Serving files in directory](#serving-files-in-directory) + - [Serving static files with authentication](#serving-static-files-with-authentication) + - [Specifying Cache-Control header](#specifying-cache-control-header) + - [Specifying Date-Modified header](#specifying-date-modified-header) + - [Specifying Template Processor callback](#specifying-template-processor-callback) + - [Serving static files by custom handling](#serving-static-files-by-custom-handling) +- [Param Rewrite With Matching](#param-rewrite-with-matching) +- [Using filters](#using-filters) + - [Serve different site files in AP mode](#serve-different-site-files-in-ap-mode) + - [Rewrite to different index on AP](#rewrite-to-different-index-on-ap) + - [Serving different hosts](#serving-different-hosts) + - [Determine interface inside callbacks](#determine-interface-inside-callbacks) +- [Bad Responses](#bad-responses) + - [Respond with content using a callback without content length to HTTP/1.0 clients](#respond-with-content-using-a-callback-without-content-length-to-http10-clients) +- [Async WebSocket Plugin](#async-websocket-plugin) + - [Async WebSocket Event](#async-websocket-event) + - [Methods for sending data to a socket client](#methods-for-sending-data-to-a-socket-client) + - [Direct access to web socket message buffer](#direct-access-to-web-socket-message-buffer) + - [Limiting the number of web socket clients](#limiting-the-number-of-web-socket-clients) +- [Async Event Source Plugin](#async-event-source-plugin) + - [Setup Event Source on the server](#setup-event-source-on-the-server) + - [Setup Event Source in the browser](#setup-event-source-in-the-browser) +- [Scanning for available WiFi Networks](#scanning-for-available-wifi-networks) +- [Remove handlers and rewrites](#remove-handlers-and-rewrites) +- [Setting up the server](#setting-up-the-server) + - [Setup global and class functions as request handlers](#setup-global-and-class-functions-as-request-handlers) + - [Methods for controlling websocket connections](#methods-for-controlling-websocket-connections) + - [Adding Default Headers](#adding-default-headers) + - [Path variable](#path-variable) + +### Why should you care + +- Using asynchronous network means that you can handle more than one connection at the same time +- You are called once the request is ready and parsed +- When you send the response, you are immediately ready to handle other connections + while the server is taking care of sending the response in the background +- Speed is OMG +- Easy to use API, HTTP Basic and Digest MD5 Authentication (default), ChunkedResponse +- Easily extendible to handle any type of content +- Supports Continue 100 +- Async WebSocket plugin offering different locations without extra servers or ports +- Async EventSource (Server-Sent Events) plugin to send events to the browser +- URL Rewrite plugin for conditional and permanent url rewrites +- ServeStatic plugin that supports cache, Last-Modified, default index and more +- Simple template processing engine to handle templates + +### Important things to remember + +- This is fully asynchronous server and as such does not run on the loop thread. +- You can not use yield or delay or any function that uses them inside the callbacks +- The server is smart enough to know when to close the connection and free resources +- You can not send more than one response to a single request + +### Principles of operation + +#### The Async Web server + +- Listens for connections +- Wraps the new clients into `Request` +- Keeps track of clients and cleans memory +- Manages `Rewrites` and apply them on the request url +- Manages `Handlers` and attaches them to Requests + +#### Request Life Cycle + +- TCP connection is received by the server +- The connection is wrapped inside `Request` object +- When the request head is received (type, url, get params, http version and host), + the server goes through all `Rewrites` (in the order they were added) to rewrite the url and inject query parameters, + next, it goes through all attached `Handlers`(in the order they were added) trying to find one + that `canHandle` the given request. If none are found, the default(catch-all) handler is attached. +- The rest of the request is received, calling the `handleUpload` or `handleBody` methods of the `Handler` if they are needed (POST+File/Body) +- When the whole request is parsed, the result is given to the `handleRequest` method of the `Handler` and is ready to be responded to +- In the `handleRequest` method, to the `Request` is attached a `Response` object (see below) that will serve the response data back to the client +- When the `Response` is sent, the client is closed and freed from the memory + +#### Rewrites and how do they work + +- The `Rewrites` are used to rewrite the request url and/or inject get parameters for a specific request url path. +- All `Rewrites` are evaluated on the request in the order they have been added to the server. +- The `Rewrite` will change the request url only if the request url (excluding get parameters) is fully match + the rewrite url, and when the optional `Filter` callback return true. +- Setting a `Filter` to the `Rewrite` enables to control when to apply the rewrite, decision can be based on + request url, http version, request host/port/target host, get parameters or the request client's localIP or remoteIP. +- Two filter callbacks are provided: `ON_AP_FILTER` to execute the rewrite when request is made to the AP interface, + `ON_STA_FILTER` to execute the rewrite when request is made to the STA interface. +- The `Rewrite` can specify a target url with optional get parameters, e.g. `/to-url?with=params` + +#### Handlers and how do they work + +- The `Handlers` are used for executing specific actions to particular requests +- One `Handler` instance can be attached to any request and lives together with the server +- Setting a `Filter` to the `Handler` enables to control when to apply the handler, decision can be based on + request url, http version, request host/port/target host, get parameters or the request client's localIP or remoteIP. +- Two filter callbacks are provided: `ON_AP_FILTER` to execute the rewrite when request is made to the AP interface, + `ON_STA_FILTER` to execute the rewrite when request is made to the STA interface. +- The `canHandle` method is used for handler specific control on whether the requests can be handled + and for declaring any interesting headers that the `Request` should parse. Decision can be based on request + method, request url, http version, request host/port/target host and get parameters +- Once a `Handler` is attached to given `Request` (`canHandle` returned true) + that `Handler` takes care to receive any file/data upload and attach a `Response` + once the `Request` has been fully parsed +- `Handlers` are evaluated in the order they are attached to the server. The `canHandle` is called only + if the `Filter` that was set to the `Handler` return true. +- The first `Handler` that can handle the request is selected, not further `Filter` and `canHandle` are called. + +#### Responses and how do they work + +- The `Response` objects are used to send the response data back to the client +- The `Response` object lives with the `Request` and is freed on end or disconnect +- Different techniques are used depending on the response type to send the data in packets + returning back almost immediately and sending the next packet when this one is received. + Any time in between is spent to run the user loop and handle other network packets +- Responding asynchronously is probably the most difficult thing for most to understand +- Many different options exist for the user to make responding a background task + +#### Template processing + +- ESPAsyncWebserver contains simple template processing engine. +- Template processing can be added to most response types. +- Currently it supports only replacing template placeholders with actual values. No conditional processing, cycles, etc. +- Placeholders are delimited with `%` symbols. Like this: `%TEMPLATE_PLACEHOLDER%`. +- It works by extracting placeholder name from response text and passing it to user provided function which should return actual value to be used instead of placeholder. +- Since it's user provided function, it is possible for library users to implement conditional processing and cycles themselves. +- Since it's impossible to know the actual response size after template processing step in advance (and, therefore, to include it in response headers), the response becomes [chunked](#chunked-response). + +### Libraries and projects that use AsyncWebServer + +- [WebSocketToSerial](https://github.com/hallard/WebSocketToSerial) - Debug serial devices through the web browser +- [Sattrack](https://github.com/Hopperpop/Sattrack) - Track the ISS with ESP8266 +- [ESP Radio](https://github.com/Edzelf/Esp-radio) - Icecast radio based on ESP8266 and VS1053 +- [VZero](https://github.com/andig/vzero) - the Wireless zero-config controller for volkszaehler.org +- [ESPurna](https://bitbucket.org/xoseperez/espurna) - ESPurna ("spark" in Catalan) is a custom C firmware for ESP8266 based smart switches. It was originally developed with the ITead Sonoff in mind. +- [fauxmoESP](https://bitbucket.org/xoseperez/fauxmoesp) - Belkin WeMo emulator library for ESP8266. +- [ESP-RFID](https://github.com/omersiar/esp-rfid) - MFRC522 RFID Access Control Management project for ESP8266. + +### Request Variables + +#### Common Variables + +```cpp +request->version(); // uint8_t: 0 = HTTP/1.0, 1 = HTTP/1.1 +request->method(); // enum: HTTP_GET, HTTP_POST, HTTP_DELETE, HTTP_PUT, HTTP_PATCH, HTTP_HEAD, HTTP_OPTIONS +request->url(); // String: URL of the request (not including host, port or GET parameters) +request->host(); // String: The requested host (can be used for virtual hosting) +request->contentType(); // String: ContentType of the request (not avaiable in Handler::canHandle) +request->contentLength(); // size_t: ContentLength of the request (not avaiable in Handler::canHandle) +request->multipart(); // bool: True if the request has content type "multipart" +``` + +#### Headers + +```cpp +//List all collected headers +int headers = request->headers(); +int i; +for(i=0;igetHeader(i); + Serial.printf("HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str()); +} + +//get specific header by name +if(request->hasHeader("MyHeader")){ + AsyncWebHeader* h = request->getHeader("MyHeader"); + Serial.printf("MyHeader: %s\n", h->value().c_str()); +} + +//List all collected headers (Compatibility) +int headers = request->headers(); +int i; +for(i=0;iheaderName(i).c_str(), request->header(i).c_str()); +} + +//get specific header by name (Compatibility) +if(request->hasHeader("MyHeader")){ + Serial.printf("MyHeader: %s\n", request->header("MyHeader").c_str()); +} +``` + +#### GET, POST and FILE parameters + +```cpp +//List all parameters +int params = request->params(); +for(int i=0;igetParam(i); + if(p->isFile()){ //p->isPost() is also true + Serial.printf("FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size()); + } else if(p->isPost()){ + Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } else { + Serial.printf("GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } +} + +//Check if GET parameter exists +if(request->hasParam("download")) + AsyncWebParameter* p = request->getParam("download"); + +//Check if POST (but not File) parameter exists +if(request->hasParam("download", true)) + AsyncWebParameter* p = request->getParam("download", true); + +//Check if FILE was uploaded +if(request->hasParam("download", true, true)) + AsyncWebParameter* p = request->getParam("download", true, true); + +//List all parameters (Compatibility) +int args = request->args(); +for(int i=0;iargName(i).c_str(), request->arg(i).c_str()); +} + +//Check if parameter exists (Compatibility) +if(request->hasArg("download")) + String arg = request->arg("download"); +``` + +#### FILE Upload handling + +```cpp +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + Serial.printf("UploadStart: %s\n", filename.c_str()); + } + for(size_t i=0; i(); + // ... +}); +server.addHandler(handler); +``` + +### Responses + +#### Redirect to another URL + +```cpp +//to local url +request->redirect("/login"); + +//to external url +request->redirect("http://esp8266.com"); +``` + +#### Basic response with HTTP Code + +```cpp +request->send(404); //Sends 404 File Not Found +``` + +#### Basic response with HTTP Code and extra headers + +```cpp +AsyncWebServerResponse *response = request->beginResponse(404); //Sends 404 File Not Found +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Basic response with string content + +```cpp +request->send(200, "text/plain", "Hello World!"); +``` + +#### Basic response with string content and extra headers + +```cpp +AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Hello World!"); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send large webpage from PROGMEM + +```cpp +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +request->send_P(200, "text/html", index_html); +``` + +#### Send large webpage from PROGMEM and extra headers + +```cpp +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send large webpage from PROGMEM containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +request->send_P(200, "text/html", index_html, processor); +``` + +#### Send large webpage from PROGMEM containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +const char index_html[] PROGMEM = "..."; // large char array, tested with 14k +AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", index_html, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Send binary content from PROGMEM + +```cpp + +//File: favicon.ico.gz, Size: 726 +#define favicon_ico_gz_len 726 +const uint8_t favicon_ico_gz[] PROGMEM = { + 0x1F, 0x8B, 0x08, 0x08, 0x0B, 0x87, 0x90, 0x57, 0x00, 0x03, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6F, + 0x6E, 0x2E, 0x69, 0x63, 0x6F, 0x00, 0xCD, 0x53, 0x5F, 0x48, 0x9A, 0x51, 0x14, 0xBF, 0x62, 0x6D, + 0x86, 0x96, 0xA9, 0x64, 0xD3, 0xFE, 0xA8, 0x99, 0x65, 0x1A, 0xB4, 0x8A, 0xA8, 0x51, 0x54, 0x23, + 0xA8, 0x11, 0x49, 0x51, 0x8A, 0x34, 0x62, 0x93, 0x85, 0x31, 0x58, 0x44, 0x12, 0x45, 0x2D, 0x58, + 0xF5, 0x52, 0x41, 0x10, 0x23, 0x82, 0xA0, 0x20, 0x98, 0x2F, 0xC1, 0x26, 0xED, 0xA1, 0x20, 0x89, + 0x04, 0xD7, 0x83, 0x58, 0x20, 0x28, 0x04, 0xAB, 0xD1, 0x9B, 0x8C, 0xE5, 0xC3, 0x60, 0x32, 0x64, + 0x0E, 0x56, 0xBF, 0x9D, 0xEF, 0xF6, 0x30, 0x82, 0xED, 0xAD, 0x87, 0xDD, 0x8F, 0xF3, 0xDD, 0x8F, + 0x73, 0xCF, 0xEF, 0x9C, 0xDF, 0x39, 0xBF, 0xFB, 0x31, 0x26, 0xA2, 0x27, 0x37, 0x97, 0xD1, 0x5B, + 0xCF, 0x9E, 0x67, 0x30, 0xA6, 0x66, 0x8C, 0x99, 0xC9, 0xC8, 0x45, 0x9E, 0x6B, 0x3F, 0x5F, 0x74, + 0xA6, 0x94, 0x5E, 0xDB, 0xFF, 0xB2, 0xE6, 0xE7, 0xE7, 0xF9, 0xDE, 0xD6, 0xD6, 0x96, 0xDB, 0xD8, + 0xD8, 0x78, 0xBF, 0xA1, 0xA1, 0xC1, 0xDA, 0xDC, 0xDC, 0x2C, 0xEB, 0xED, 0xED, 0x15, 0x9B, 0xCD, + 0xE6, 0x4A, 0x83, 0xC1, 0xE0, 0x2E, 0x29, 0x29, 0x99, 0xD6, 0x6A, 0xB5, 0x4F, 0x75, 0x3A, 0x9D, + 0x61, 0x75, 0x75, 0x95, 0xB5, 0xB7, 0xB7, 0xDF, 0xC8, 0xD1, 0xD4, 0xD4, 0xF4, 0xB0, 0xBA, 0xBA, + 0xFA, 0x83, 0xD5, 0x6A, 0xFD, 0x5A, 0x5E, 0x5E, 0x9E, 0x28, 0x2D, 0x2D, 0x0D, 0x10, 0xC6, 0x4B, + 0x98, 0x78, 0x5E, 0x5E, 0xDE, 0x95, 0x42, 0xA1, 0x40, 0x4E, 0x4E, 0xCE, 0x65, 0x76, 0x76, 0xF6, + 0x47, 0xB5, 0x5A, 0x6D, 0x4F, 0x26, 0x93, 0xA2, 0xD6, 0xD6, 0x56, 0x8E, 0x6D, 0x69, 0x69, 0xD1, + 0x11, 0x36, 0x62, 0xB1, 0x58, 0x60, 0x32, 0x99, 0xA0, 0xD7, 0xEB, 0x51, 0x58, 0x58, 0x88, 0xFC, + 0xFC, 0x7C, 0x10, 0x16, 0x02, 0x56, 0x2E, 0x97, 0x43, 0x2A, 0x95, 0x42, 0x2C, 0x16, 0x23, 0x33, + 0x33, 0x33, 0xAE, 0x52, 0xA9, 0x1E, 0x64, 0x65, 0x65, 0x71, 0x7C, 0x7D, 0x7D, 0xBD, 0x93, 0xEA, + 0xFE, 0x30, 0x1A, 0x8D, 0xE8, 0xEC, 0xEC, 0xC4, 0xE2, 0xE2, 0x22, 0x6A, 0x6A, 0x6A, 0x40, 0x39, + 0x41, 0xB5, 0x38, 0x4E, 0xC8, 0x33, 0x3C, 0x3C, 0x0C, 0x87, 0xC3, 0xC1, 0x6B, 0x54, 0x54, 0x54, + 0xBC, 0xE9, 0xEB, 0xEB, 0x93, 0x5F, 0x5C, 0x5C, 0x30, 0x8A, 0x9D, 0x2E, 0x2B, 0x2B, 0xBB, 0xA2, + 0x3E, 0x41, 0xBD, 0x21, 0x1E, 0x8F, 0x63, 0x6A, 0x6A, 0x0A, 0x81, 0x40, 0x00, 0x94, 0x1B, 0x3D, + 0x3D, 0x3D, 0x42, 0x3C, 0x96, 0x96, 0x96, 0x70, 0x7E, 0x7E, 0x8E, 0xE3, 0xE3, 0x63, 0xF8, 0xFD, + 0xFE, 0xB4, 0xD7, 0xEB, 0xF5, 0x8F, 0x8F, 0x8F, 0x5B, 0x68, 0x5E, 0x6F, 0x05, 0xCE, 0xB4, 0xE3, + 0xE8, 0xE8, 0x08, 0x27, 0x27, 0x27, 0xD8, 0xDF, 0xDF, 0xC7, 0xD9, 0xD9, 0x19, 0x6C, 0x36, 0x1B, + 0x36, 0x36, 0x36, 0x38, 0x9F, 0x85, 0x85, 0x05, 0xAC, 0xAF, 0xAF, 0x23, 0x1A, 0x8D, 0x22, 0x91, + 0x48, 0x20, 0x16, 0x8B, 0xFD, 0xDA, 0xDA, 0xDA, 0x7A, 0x41, 0x33, 0x7E, 0x57, 0x50, 0x50, 0x80, + 0x89, 0x89, 0x09, 0x84, 0xC3, 0x61, 0x6C, 0x6F, 0x6F, 0x23, 0x12, 0x89, 0xE0, 0xE0, 0xE0, 0x00, + 0x43, 0x43, 0x43, 0x58, 0x5E, 0x5E, 0xE6, 0x9C, 0x7D, 0x3E, 0x1F, 0x46, 0x47, 0x47, 0x79, 0xBE, + 0xBD, 0xBD, 0x3D, 0xE1, 0x3C, 0x1D, 0x0C, 0x06, 0x9F, 0x10, 0xB7, 0xC7, 0x84, 0x4F, 0xF6, 0xF7, + 0xF7, 0x63, 0x60, 0x60, 0x00, 0x83, 0x83, 0x83, 0x18, 0x19, 0x19, 0xC1, 0xDC, 0xDC, 0x1C, 0x8F, + 0x17, 0x7C, 0xA4, 0x27, 0xE7, 0x34, 0x39, 0x39, 0x89, 0x9D, 0x9D, 0x1D, 0x6E, 0x54, 0xE3, 0x13, + 0xE5, 0x34, 0x11, 0x37, 0x49, 0x51, 0x51, 0xD1, 0x4B, 0xA5, 0x52, 0xF9, 0x45, 0x26, 0x93, 0x5D, + 0x0A, 0xF3, 0x92, 0x48, 0x24, 0xA0, 0x6F, 0x14, 0x17, 0x17, 0xA3, 0xB6, 0xB6, 0x16, 0x5D, 0x5D, + 0x5D, 0x7C, 0x1E, 0xBB, 0xBB, 0xBB, 0x9C, 0xD7, 0xE1, 0xE1, 0x21, 0x42, 0xA1, 0xD0, 0x6B, 0xD2, + 0x45, 0x4C, 0x33, 0x12, 0x34, 0xCC, 0xA0, 0x19, 0x54, 0x92, 0x56, 0x0E, 0xD2, 0xD9, 0x43, 0xF8, + 0xCF, 0x82, 0x56, 0xC2, 0xDC, 0xEB, 0xEA, 0xEA, 0x38, 0x7E, 0x6C, 0x6C, 0x4C, 0xE0, 0xFE, 0x9D, + 0xB8, 0xBF, 0xA7, 0xFA, 0xAF, 0x56, 0x56, 0x56, 0xEE, 0x6D, 0x6E, 0x6E, 0xDE, 0xB8, 0x47, 0x55, + 0x55, 0x55, 0x6C, 0x66, 0x66, 0x46, 0x44, 0xDA, 0x3B, 0x34, 0x1A, 0x4D, 0x94, 0xB0, 0x3F, 0x09, + 0x7B, 0x45, 0xBD, 0xA5, 0x5D, 0x2E, 0x57, 0x8C, 0x7A, 0x73, 0xD9, 0xED, 0xF6, 0x3B, 0x84, 0xFF, + 0xE7, 0x7D, 0xA6, 0x3A, 0x2C, 0x95, 0x4A, 0xB1, 0x8E, 0x8E, 0x0E, 0x6D, 0x77, 0x77, 0xB7, 0xCD, + 0xE9, 0x74, 0x3E, 0x73, 0xBB, 0xDD, 0x8F, 0x3C, 0x1E, 0x8F, 0xE6, 0xF4, 0xF4, 0x94, 0xAD, 0xAD, + 0xAD, 0xDD, 0xDE, 0xCF, 0x73, 0x0B, 0x0B, 0xB8, 0xB6, 0xE0, 0x5D, 0xC6, 0x66, 0xC5, 0xE4, 0x10, + 0x4C, 0xF4, 0xF7, 0xD8, 0x59, 0xF2, 0x7F, 0xA3, 0xB8, 0xB4, 0xFC, 0x0F, 0xEE, 0x37, 0x70, 0xEC, + 0x16, 0x4A, 0x7E, 0x04, 0x00, 0x00 +}; + +AsyncWebServerResponse *response = request->beginResponse_P(200, "image/x-icon", favicon_ico_gz, favicon_ico_gz_len); +response->addHeader("Content-Encoding", "gzip"); +request->send(response); +``` + +#### Respond with content coming from a Stream + +```cpp +//read 12 bytes from Serial and send them as Content Type text/plain +request->send(Serial, "text/plain", 12); +``` + +#### Respond with content coming from a Stream and extra headers + +```cpp +//read 12 bytes from Serial and send them as Content Type text/plain +AsyncWebServerResponse *response = request->beginResponse(Serial, "text/plain", 12); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a Stream containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//read 12 bytes from Serial and send them as Content Type text/plain +request->send(Serial, "text/plain", 12, processor); +``` + +#### Respond with content coming from a Stream containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//read 12 bytes from Serial and send them as Content Type text/plain +AsyncWebServerResponse *response = request->beginResponse(Serial, "text/plain", 12, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a File + +```cpp +//Send index.htm with default content type +request->send(SPIFFS, "/index.htm"); + +//Send index.htm as text +request->send(SPIFFS, "/index.htm", "text/plain"); + +//Download index.htm +request->send(SPIFFS, "/index.htm", String(), true); +``` + +#### Respond with content coming from a File and extra headers + +```cpp +//Send index.htm with default content type +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm"); + +//Send index.htm as text +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm", "text/plain"); + +//Download index.htm +AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/index.htm", String(), true); + +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with content coming from a File containing templates + +Internally uses [Chunked Response](#chunked-response). + +Index.htm contents: + +``` +%HELLO_FROM_TEMPLATE% +``` + +Somewhere in source files: + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//Send index.htm with template processor function +request->send(SPIFFS, "/index.htm", String(), false, processor); +``` + +#### Respond with content using a callback + +```cpp +//send 128 bytes as plain text +request->send("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}); +``` + +#### Respond with content using a callback and extra headers + +```cpp +//send 128 bytes as plain text +AsyncWebServerResponse *response = request->beginResponse("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Respond with file content using a callback and extra headers + +With this code your ESP is able to serve even large (large in terms of ESP, e.g. 100kB) files +without memory problems. + +You need to create a file handler in outer function (to have a single one for request) but use +it in a lambda. The catch is that the lambda has it's own lifecycle which may/will cause it's +called after the original function is over thus the original file handle is destroyed. Using the +captured `&file` in the lambda then causes segfault (Hello, Exception 9!) and the whole ESP crashes. +By using this code, you tell the compiler to move the handle into the lambda so it won't be +destroyed when outer function (that one where you call `request->send(response)`) ends. + +```cpp +const File file = ... // e.g. SPIFFS.open(path, "r"); + +const contentType = "application/javascript"; + +AsyncWebServerResponse *response = request->beginResponse( + contentType, + file.size(), + [file](uint8_t *buffer, size_t maxLen, size_t total) mutable -> size_t { + int bytes = file.read(buffer, maxLen); + + // close file at the end + if (bytes + total == file.size()) file.close(); + + return max(0, bytes); // return 0 even when no bytes were loaded + } +); + +if (gzipped) { + response->addHeader(F("Content-Encoding"), F("gzip")); +} + +request->send(response); +``` + +#### Respond with content using a callback containing templates + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//send 128 bytes as plain text +request->send("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}, processor); +``` + +#### Respond with content using a callback containing templates and extra headers + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +//send 128 bytes as plain text +AsyncWebServerResponse *response = request->beginResponse("text/plain", 128, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will not be asked for more bytes once the content length has been reached. + //Keep in mind that you can not delay or yield waiting for more data! + //Send what you currently have and you will be asked for more again + return mySource.read(buffer, maxLen); +}, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Chunked Response + +Used when content length is unknown. Works best if the client supports HTTP/1.1 + +```cpp +AsyncWebServerResponse *response = request->beginChunkedResponse("text/plain", [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Chunked Response containing templates + +Used when content length is unknown. Works best if the client supports HTTP/1.1 + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +AsyncWebServerResponse *response = request->beginChunkedResponse("text/plain", [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //index equals the amount of bytes that have been already sent + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}, processor); +response->addHeader("Server","ESP Async Web Server"); +request->send(response); +``` + +#### Print to response + +```cpp +AsyncResponseStream *response = request->beginResponseStream("text/html"); +response->addHeader("Server","ESP Async Web Server"); +response->printf("Webpage at %s", request->url().c_str()); + +response->print("

Hello "); +response->print(request->client()->remoteIP()); +response->print("

"); + +response->print("

General

"); +response->print("
    "); +response->printf("
  • Version: HTTP/1.%u
  • ", request->version()); +response->printf("
  • Method: %s
  • ", request->methodToString()); +response->printf("
  • URL: %s
  • ", request->url().c_str()); +response->printf("
  • Host: %s
  • ", request->host().c_str()); +response->printf("
  • ContentType: %s
  • ", request->contentType().c_str()); +response->printf("
  • ContentLength: %u
  • ", request->contentLength()); +response->printf("
  • Multipart: %s
  • ", request->multipart()?"true":"false"); +response->print("
"); + +response->print("

Headers

"); +response->print("
    "); +int headers = request->headers(); +for(int i=0;igetHeader(i); + response->printf("
  • %s: %s
  • ", h->name().c_str(), h->value().c_str()); +} +response->print("
"); + +response->print("

Parameters

"); +response->print("
    "); +int params = request->params(); +for(int i=0;igetParam(i); + if(p->isFile()){ + response->printf("
  • FILE[%s]: %s, size: %u
  • ", p->name().c_str(), p->value().c_str(), p->size()); + } else if(p->isPost()){ + response->printf("
  • POST[%s]: %s
  • ", p->name().c_str(), p->value().c_str()); + } else { + response->printf("
  • GET[%s]: %s
  • ", p->name().c_str(), p->value().c_str()); + } +} +response->print("
"); + +response->print(""); +//send the response last +request->send(response); +``` + +#### ArduinoJson Basic Response + +This way of sending Json is great for when the result is below 4KB + +```cpp +#include "AsyncJson.h" +#include "ArduinoJson.h" + + +AsyncResponseStream *response = request->beginResponseStream("application/json"); +DynamicJsonBuffer jsonBuffer; +JsonObject &root = jsonBuffer.createObject(); +root["heap"] = ESP.getFreeHeap(); +root["ssid"] = WiFi.SSID(); +root.printTo(*response); +request->send(response); +``` + +#### ArduinoJson Advanced Response + +This response can handle really large Json objects (tested to 40KB) +There isn't any noticeable speed decrease for small results with the method above +Since ArduinoJson does not allow reading parts of the string, the whole Json has to +be passed every time a chunks needs to be sent, which shows speed decrease proportional +to the resulting json packets + +```cpp +#include "AsyncJson.h" +#include "ArduinoJson.h" + + +AsyncJsonResponse * response = new AsyncJsonResponse(); +response->addHeader("Server","ESP Async Web Server"); +JsonObject& root = response->getRoot(); +root["heap"] = ESP.getFreeHeap(); +root["ssid"] = WiFi.SSID(); +response->setLength(); +request->send(response); +``` + +### Serving static files + +In addition to serving files from SPIFFS as described above, the server provide a dedicated handler that optimize the +performance of serving files from SPIFFS - `AsyncStaticWebHandler`. Use `server.serveStatic()` function to +initialize and add a new instance of `AsyncStaticWebHandler` to the server. +The Handler will not handle the request if the file does not exists, e.g. the server will continue to look for another +handler that can handle the request. +Notice that you can chain setter functions to setup the handler, or keep a pointer to change it at a later time. + +#### Serving specific file by name + +```cpp +// Serve the file "/www/page.htm" when request url is "/page.htm" +server.serveStatic("/page.htm", SPIFFS, "/www/page.htm"); +``` + +#### Serving files in directory + +To serve files in a directory, the path to the files should specify a directory in SPIFFS and ends with "/". + +```cpp +// Serve files in directory "/www/" when request url starts with "/" +// Request to the root or none existing files will try to server the defualt +// file name "index.htm" if exists +server.serveStatic("/", SPIFFS, "/www/"); + +// Server with different default file +server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("default.html"); +``` + +#### Serving static files with authentication + +```cpp +server + .serveStatic("/", SPIFFS, "/www/") + .setDefaultFile("default.html") + .setAuthentication("user", "pass"); +``` + +#### Specifying Cache-Control header + +It is possible to specify Cache-Control header value to reduce the number of calls to the server once the client loaded +the files. For more information on Cache-Control values see [Cache-Control](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) + +```cpp +// Cache responses for 10 minutes (600 seconds) +server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=600"); + +//*** Change Cache-Control after server setup *** + +// During setup - keep a pointer to the handler +AsyncStaticWebHandler* handler = &server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=600"); + +// At a later event - change Cache-Control +handler->setCacheControl("max-age=30"); +``` + +#### Specifying Date-Modified header + +It is possible to specify Date-Modified header to enable the server to return Not-Modified (304) response for requests +with "If-Modified-Since" header with the same value, instead of responding with the actual file content. + +```cpp +// Update the date modified string every time files are updated +server.serveStatic("/", SPIFFS, "/www/").setLastModified("Mon, 20 Jun 2016 14:00:00 GMT"); + +//*** Chage last modified value at a later stage *** + +// During setup - read last modified value from config or EEPROM +String date_modified = loadDateModified(); +AsyncStaticWebHandler* handler = &server.serveStatic("/", SPIFFS, "/www/"); +handler->setLastModified(date_modified); + +// At a later event when files are updated +String date_modified = getNewDateModfied(); +saveDateModified(date_modified); // Save for next reset +handler->setLastModified(date_modified); +``` + +#### Specifying Template Processor callback + +It is possible to specify template processor for static files. For information on template processor see +[Respond with content coming from a File containing templates](#respond-with-content-coming-from-a-file-containing-templates). + +```cpp +String processor(const String& var) +{ + if(var == "HELLO_FROM_TEMPLATE") + return F("Hello world!"); + return String(); +} + +// ... + +server.serveStatic("/", SPIFFS, "/www/").setTemplateProcessor(processor); +``` + +#### Serving static files by custom handling + +It may happen your static files are too big and the ESP will crash the request before it sends the whole file. +In that case, you can handle static files with custom file serving through not found handler. + +This code below is more-or-less equivalent to this: + +```cpp +webServer.serveStatic("/", SPIFFS, STATIC_FILES_PREFIX).setDefaultFile("index.html") +``` + +First, declare the handling function: + +```cpp +bool handleStaticFile(AsyncWebServerRequest *request) { + String path = STATIC_FILES_PREFIX + request->url(); + + if (path.endsWith("/")) path += F("index.html"); + + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + + if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + bool gzipped = false; + if (SPIFFS.exists(pathWithGz)) { + gzipped = true; + path += ".gz"; + } + + // TODO serve the file + + return true; + } + + return false; +} +``` + +And then configure your webserver: + +```cpp +webServer.onNotFound([](AsyncWebServerRequest *request) { + if (handleStaticFile(request)) return; + + request->send(404); +}); +``` + +You may want to try [Respond with file content using a callback and extra headers](#respond-with-file-content-using-a-callback-and-extra-headers) +For actual serving the file. + +### Param Rewrite With Matching + +It is possible to rewrite the request url with parameter matchg. Here is an example with one parameter: +Rewrite for example "/radio/{frequence}" -> "/radio?f={frequence}" + +```cpp +class OneParamRewrite : public AsyncWebRewrite +{ + protected: + String _urlPrefix; + int _paramIndex; + String _paramsBackup; + + public: + OneParamRewrite(const char* from, const char* to) + : AsyncWebRewrite(from, to) { + + _paramIndex = _from.indexOf('{'); + + if( _paramIndex >=0 && _from.endsWith("}")) { + _urlPrefix = _from.substring(0, _paramIndex); + int index = _params.indexOf('{'); + if(index >= 0) { + _params = _params.substring(0, index); + } + } else { + _urlPrefix = _from; + } + _paramsBackup = _params; + } + + bool match(AsyncWebServerRequest *request) override { + if(request->url().startsWith(_urlPrefix)) { + if(_paramIndex >= 0) { + _params = _paramsBackup + request->url().substring(_paramIndex); + } else { + _params = _paramsBackup; + } + return true; + + } else { + return false; + } + } +}; +``` + +Usage: + +```cpp + server.addRewrite( new OneParamRewrite("/radio/{frequence}", "/radio?f={frequence}") ); +``` + +### Using filters + +Filters can be set to `Rewrite` or `Handler` in order to control when to apply the rewrite and consider the handler. +A filter is a callback function that evaluates the request and return a boolean `true` to include the item +or `false` to exclude it. +Two filter callback are provided for convince: + +- `ON_STA_FILTER` - return true when requests are made to the STA (station mode) interface. +- `ON_AP_FILTER` - return true when requests are made to the AP (access point) interface. + +#### Serve different site files in AP mode + +```cpp +server.serveStatic("/", SPIFFS, "/www/").setFilter(ON_STA_FILTER); +server.serveStatic("/", SPIFFS, "/ap/").setFilter(ON_AP_FILTER); +``` + +#### Rewrite to different index on AP + +```cpp +// Serve the file "/www/index-ap.htm" in AP, and the file "/www/index.htm" on STA +server.rewrite("/", "index.htm"); +server.rewrite("/index.htm", "index-ap.htm").setFilter(ON_AP_FILTER); +server.serveStatic("/", SPIFFS, "/www/"); +``` + +#### Serving different hosts + +```cpp +// Filter callback using request host +bool filterOnHost1(AsyncWebServerRequest *request) { return request->host() == "host1"; } + +// Server setup: server files in "/host1/" to requests for "host1", and files in "/www/" otherwise. +server.serveStatic("/", SPIFFS, "/host1/").setFilter(filterOnHost1); +server.serveStatic("/", SPIFFS, "/www/"); +``` + +#### Determine interface inside callbacks + +```cpp + String RedirectUrl = "http://"; + if (ON_STA_FILTER(request)) { + RedirectUrl += WiFi.localIP().toString(); + } else { + RedirectUrl += WiFi.softAPIP().toString(); + } + RedirectUrl += "/index.htm"; + request->redirect(RedirectUrl); +``` + +### Bad Responses + +Some responses are implemented, but you should not use them, because they do not conform to HTTP. +The following example will lead to unclean close of the connection and more time wasted +than providing the length of the content + +#### Respond with content using a callback without content length to HTTP/1.0 clients + +```cpp +//This is used as fallback for chunked responses to HTTP/1.0 Clients +request->send("text/plain", 0, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + //Write up to "maxLen" bytes into "buffer" and return the amount written. + //You will be asked for more data until 0 is returned + //Keep in mind that you can not delay or yield waiting for more data! + return mySource.read(buffer, maxLen); +}); +``` + +### Async WebSocket Plugin + +The server includes a web socket plugin which lets you define different WebSocket locations to connect to +without starting another listening service or using different port + +#### Async WebSocket Event + +```cpp + +void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + if(type == WS_EVT_CONNECT){ + //client connected + os_printf("ws[%s][%u] connect\n", server->url(), client->id()); + client->printf("Hello Client %u :)", client->id()); + client->ping(); + } else if(type == WS_EVT_DISCONNECT){ + //client disconnected + os_printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); + } else if(type == WS_EVT_ERROR){ + //error was received from the other end + os_printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); + } else if(type == WS_EVT_PONG){ + //pong message was received (in response to a ping request maybe) + os_printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); + } else if(type == WS_EVT_DATA){ + //data packet + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if(info->final && info->index == 0 && info->len == len){ + //the whole message is in a single frame and we got all of it's data + os_printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); + if(info->opcode == WS_TEXT){ + data[len] = 0; + os_printf("%s\n", (char*)data); + } else { + for(size_t i=0; i < info->len; i++){ + os_printf("%02x ", data[i]); + } + os_printf("\n"); + } + if(info->opcode == WS_TEXT) + client->text("I got your text message"); + else + client->binary("I got your binary message"); + } else { + //message is comprised of multiple frames or the frame is split into multiple packets + if(info->index == 0){ + if(info->num == 0) + os_printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + os_printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); + } + + os_printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); + if(info->message_opcode == WS_TEXT){ + data[len] = 0; + os_printf("%s\n", (char*)data); + } else { + for(size_t i=0; i < len; i++){ + os_printf("%02x ", data[i]); + } + os_printf("\n"); + } + + if((info->index + len) == info->len){ + os_printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); + if(info->final){ + os_printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + if(info->message_opcode == WS_TEXT) + client->text("I got your text message"); + else + client->binary("I got your binary message"); + } + } + } + } +} +``` + +#### Methods for sending data to a socket client + +```cpp + + + +//Server methods +AsyncWebSocket ws("/ws"); +//printf to a client +ws.printf((uint32_t)client_id, arguments...); +//printf to all clients +ws.printfAll(arguments...); +//printf_P to a client +ws.printf_P((uint32_t)client_id, PSTR(format), arguments...); +//printfAll_P to all clients +ws.printfAll_P(PSTR(format), arguments...); +//send text to a client +ws.text((uint32_t)client_id, (char*)text); +ws.text((uint32_t)client_id, (uint8_t*)text, (size_t)len); +//send text from PROGMEM to a client +ws.text((uint32_t)client_id, PSTR("text")); +const char flash_text[] PROGMEM = "Text to send" +ws.text((uint32_t)client_id, FPSTR(flash_text)); +//send text to all clients +ws.textAll((char*)text); +ws.textAll((uint8_t*)text, (size_t)len); +//send binary to a client +ws.binary((uint32_t)client_id, (char*)binary); +ws.binary((uint32_t)client_id, (uint8_t*)binary, (size_t)len); +//send binary from PROGMEM to a client +const uint8_t flash_binary[] PROGMEM = { 0x01, 0x02, 0x03, 0x04 }; +ws.binary((uint32_t)client_id, flash_binary, 4); +//send binary to all clients +ws.binaryAll((char*)binary); +ws.binaryAll((uint8_t*)binary, (size_t)len); +//HTTP Authenticate before switch to Websocket protocol +ws.setAuthentication("user", "pass"); + +//client methods +AsyncWebSocketClient * client; +//printf +client->printf(arguments...); +//printf_P +client->printf_P(PSTR(format), arguments...); +//send text +client->text((char*)text); +client->text((uint8_t*)text, (size_t)len); +//send text from PROGMEM +client->text(PSTR("text")); +const char flash_text[] PROGMEM = "Text to send"; +client->text(FPSTR(flash_text)); +//send binary +client->binary((char*)binary); +client->binary((uint8_t*)binary, (size_t)len); +//send binary from PROGMEM +const uint8_t flash_binary[] PROGMEM = { 0x01, 0x02, 0x03, 0x04 }; +client->binary(flash_binary, 4); +``` + +#### Direct access to web socket message buffer + +When sending a web socket message using the above methods a buffer is created. Under certain circumstances you might want to manipulate or populate this buffer directly from your application, for example to prevent unnecessary duplications of the data. This example below shows how to create a buffer and print data to it from an ArduinoJson object then send it. + +```cpp +void sendDataWs(AsyncWebSocketClient * client) +{ + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + root["a"] = "abc"; + root["b"] = "abcd"; + root["c"] = "abcde"; + root["d"] = "abcdef"; + root["e"] = "abcdefg"; + size_t len = root.measureLength(); + AsyncWebSocketMessageBuffer * buffer = ws.makeBuffer(len); // creates a buffer (len + 1) for you. + if (buffer) { + root.printTo((char *)buffer->get(), len + 1); + if (client) { + client->text(buffer); + } else { + ws.textAll(buffer); + } + } +} +``` + +#### Limiting the number of web socket clients + +Browsers sometimes do not correctly close the websocket connection, even when the close() function is called in javascript. This will eventually exhaust the web server's resources and will cause the server to crash. Periodically calling the cleanClients() function from the main loop() function limits the number of clients by closing the oldest client when the maximum number of clients has been exceeded. This can called be every cycle, however, if you wish to use less power, then calling as infrequently as once per second is sufficient. + +```cpp +void loop(){ + ws.cleanupClients(); +} +``` + +### Async Event Source Plugin + +The server includes EventSource (Server-Sent Events) plugin which can be used to send short text events to the browser. +Difference between EventSource and WebSockets is that EventSource is single direction, text-only protocol. + +#### Setup Event Source on the server + +```cpp +AsyncWebServer server(80); +AsyncEventSource events("/events"); + +void setup(){ + // setup ...... + events.onConnect([](AsyncEventSourceClient *client){ + if(client->lastId()){ + Serial.printf("Client reconnected! Last message ID that it gat is: %u\n", client->lastId()); + } + //send event with message "hello!", id current millis + // and set reconnect delay to 1 second + client->send("hello!",NULL,millis(),1000); + }); + //HTTP Basic authentication + events.setAuthentication("user", "pass"); + server.addHandler(&events); + // setup ...... +} + +void loop(){ + if(eventTriggered){ // your logic here + //send event "myevent" + events.send("my event content","myevent",millis()); + } +} +``` + +#### Setup Event Source in the browser + +```javascript +if (!!window.EventSource) { + var source = new EventSource("/events"); + + source.addEventListener( + "open", + function (e) { + console.log("Events Connected"); + }, + false + ); + + source.addEventListener( + "error", + function (e) { + if (e.target.readyState != EventSource.OPEN) { + console.log("Events Disconnected"); + } + }, + false + ); + + source.addEventListener( + "message", + function (e) { + console.log("message", e.data); + }, + false + ); + + source.addEventListener( + "myevent", + function (e) { + console.log("myevent", e.data); + }, + false + ); +} +``` + +### Scanning for available WiFi Networks + +```cpp +//First request will return 0 results unless you start scan from somewhere else (loop/setup) +//Do not request more often than 3-5 seconds +server.on("/scan", HTTP_GET, [](AsyncWebServerRequest *request){ + String json = "["; + int n = WiFi.scanComplete(); + if(n == -2){ + WiFi.scanNetworks(true); + } else if(n){ + for (int i = 0; i < n; ++i){ + if(i) json += ","; + json += "{"; + json += "\"rssi\":"+String(WiFi.RSSI(i)); + json += ",\"ssid\":\""+WiFi.SSID(i)+"\""; + json += ",\"bssid\":\""+WiFi.BSSIDstr(i)+"\""; + json += ",\"channel\":"+String(WiFi.channel(i)); + json += ",\"secure\":"+String(WiFi.encryptionType(i)); + json += ",\"hidden\":"+String(WiFi.isHidden(i)?"true":"false"); + json += "}"; + } + WiFi.scanDelete(); + if(WiFi.scanComplete() == -2){ + WiFi.scanNetworks(true); + } + } + json += "]"; + request->send(200, "application/json", json); + json = String(); +}); +``` + +### Remove handlers and rewrites + +Server goes through handlers in same order as they were added. You can't simple add handler with same path to override them. +To remove handler: + +```arduino +// save callback for particular URL path +auto handler = server.on("/some/path", [](AsyncWebServerRequest *request){ + //do something useful +}); +// when you don't need handler anymore remove it +server.removeHandler(&handler); + +// same with rewrites +server.removeRewrite(&someRewrite); + +server.onNotFound([](AsyncWebServerRequest *request){ + request->send(404); +}); + +// remove server.onNotFound handler +server.onNotFound(NULL); + +// remove all rewrites, handlers and onNotFound/onFileUpload/onRequestBody callbacks +server.reset(); +``` + +### Setting up the server + +```cpp +#include "ESPAsyncTCP.h" +#include "ESPAsyncWebServer.h" + +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws +AsyncEventSource events("/events"); // event source (Server-Sent events) + +const char* ssid = "your-ssid"; +const char* password = "your-pass"; +const char* http_username = "admin"; +const char* http_password = "admin"; + +//flag to use from web update to reboot the ESP +bool shouldReboot = false; + +void onRequest(AsyncWebServerRequest *request){ + //Handle Unknown Request + request->send(404); +} + +void onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + //Handle body +} + +void onUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + //Handle upload +} + +void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ + //Handle WebSocket event +} + +void setup(){ + Serial.begin(115200); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.printf("WiFi Failed!\n"); + return; + } + + // attach AsyncWebSocket + ws.onEvent(onEvent); + server.addHandler(&ws); + + // attach AsyncEventSource + server.addHandler(&events); + + // respond to GET requests on URL /heap + server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, "text/plain", String(ESP.getFreeHeap())); + }); + + // upload a file to /upload + server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ + request->send(200); + }, onUpload); + + // send a file when /index is requested + server.on("/index", HTTP_ANY, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/index.htm"); + }); + + // HTTP basic authentication + server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + request->send(200, "text/plain", "Login Success!"); + }); + + // Simple Firmware Update Form + server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, "text/html", "
"); + }); + server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request){ + shouldReboot = !Update.hasError(); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot?"OK":"FAIL"); + response->addHeader("Connection", "close"); + request->send(response); + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + if(!index){ + Serial.printf("Update Start: %s\n", filename.c_str()); + Update.runAsync(true); + if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)){ + Update.printError(Serial); + } + } + if(!Update.hasError()){ + if(Update.write(data, len) != len){ + Update.printError(Serial); + } + } + if(final){ + if(Update.end(true)){ + Serial.printf("Update Success: %uB\n", index+len); + } else { + Update.printError(Serial); + } + } + }); + + // attach filesystem root at URL /fs + server.serveStatic("/fs", SPIFFS, "/"); + + // Catch-All Handlers + // Any request that can not find a Handler that canHandle it + // ends in the callbacks below. + server.onNotFound(onRequest); + server.onFileUpload(onUpload); + server.onRequestBody(onBody); + + server.begin(); +} + +void loop(){ + if(shouldReboot){ + Serial.println("Rebooting..."); + delay(100); + ESP.restart(); + } + static char temp[128]; + sprintf(temp, "Seconds since boot: %u", millis()/1000); + events.send(temp, "time"); //send event "time" +} +``` + +#### Setup global and class functions as request handlers + +```cpp +#include +#include +#include +#include + +void handleRequest(AsyncWebServerRequest *request){} + +class WebClass { +public : + AsyncWebServer classWebServer = AsyncWebServer(81); + + WebClass(){}; + + void classRequest (AsyncWebServerRequest *request){} + + void begin(){ + // attach global request handler + classWebServer.on("/example", HTTP_ANY, handleRequest); + + // attach class request handler + classWebServer.on("/example", HTTP_ANY, std::bind(&WebClass::classRequest, this, std::placeholders::_1)); + } +}; + +AsyncWebServer globalWebServer(80); +WebClass webClassInstance; + +void setup() { + // attach global request handler + globalWebServer.on("/example", HTTP_ANY, handleRequest); + + // attach class request handler + globalWebServer.on("/example", HTTP_ANY, std::bind(&WebClass::classRequest, webClassInstance, std::placeholders::_1)); +} + +void loop() { + +} +``` + +#### Methods for controlling websocket connections + +```cpp + // Disable client connections if it was activated + if ( ws.enabled() ) + ws.enable(false); + + // enable client connections if it was disabled + if ( !ws.enabled() ) + ws.enable(true); +``` + +Example of OTA code + +```cpp + // OTA callbacks + ArduinoOTA.onStart([]() { + // Clean SPIFFS + SPIFFS.end(); + + // Disable client connections + ws.enable(false); + + // Advertise connected clients what's going on + ws.textAll("OTA Update Started"); + + // Close them + ws.closeAll(); + + }); + +``` + +#### Adding Default Headers + +In some cases, such as when working with CORS, or with some sort of custom authentication system, +you might need to define a header that should get added to all responses (including static, websocket and EventSource). +The DefaultHeaders singleton allows you to do this. + +Example: + +```cpp +DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); +webServer.begin(); +``` + +_NOTE_: You will still need to respond to the OPTIONS method for CORS pre-flight in most cases. (unless you are only using GET) + +This is one option: + +```cpp +webServer.onNotFound([](AsyncWebServerRequest *request) { + if (request->method() == HTTP_OPTIONS) { + request->send(200); + } else { + request->send(404); + } +}); +``` + +#### Path variable + +With path variable you can create a custom regex rule for a specific parameter in a route. +For example we want a `sensorId` parameter in a route rule to match only a integer. + +```cpp + server.on("^\\/sensor\\/([0-9]+)$", HTTP_GET, [] (AsyncWebServerRequest *request) { + String sensorId = request->pathArg(0); + }); +``` + +_NOTE_: All regex patterns starts with `^` and ends with `$` + +To enable the `Path variable` support, you have to define the buildflag `-DASYNCWEBSERVER_REGEX`. + +For Arduino IDE create/update `platform.local.txt`: + +`Windows`: C:\Users\(username)\AppData\Local\Arduino15\packages\\`{espxxxx}`\hardware\\`espxxxx`\\`{version}`\platform.local.txt + +`Linux`: ~/.arduino15/packages/`{espxxxx}`/hardware/`{espxxxx}`/`{version}`/platform.local.txt + +Add/Update the following line: + +``` + compiler.cpp.extra_flags=-DDASYNCWEBSERVER_REGEX +``` + +For platformio modify `platformio.ini`: + +```ini +[env:myboard] +build_flags = + -DASYNCWEBSERVER_REGEX +``` + +_NOTE_: By enabling `ASYNCWEBSERVER_REGEX`, `` will be included. This will add an 100k to your binary. diff --git a/examples/CaptivePortal/CaptivePortal.ino b/examples/CaptivePortal/CaptivePortal.ino index 2cc5851..ed9dfff 100644 --- a/examples/CaptivePortal/CaptivePortal.ino +++ b/examples/CaptivePortal/CaptivePortal.ino @@ -20,7 +20,6 @@ class CaptiveRequestHandler : public AsyncWebHandler { virtual ~CaptiveRequestHandler() {} bool canHandle(__unused AsyncWebServerRequest* request) { - // request->addInterestingHeader("ANY"); return true; } diff --git a/examples/Filters/Filters.ino b/examples/Filters/Filters.ino index b7bbcff..de5129a 100644 --- a/examples/Filters/Filters.ino +++ b/examples/Filters/Filters.ino @@ -22,7 +22,6 @@ class CaptiveRequestHandler : public AsyncWebHandler { virtual ~CaptiveRequestHandler() {} bool canHandle(__unused AsyncWebServerRequest* request) { - // request->addInterestingHeader("ANY"); return true; } diff --git a/examples/SimpleServer/SimpleServer.ino b/examples/SimpleServer/SimpleServer.ino index 2a2079a..899c77d 100644 --- a/examples/SimpleServer/SimpleServer.ino +++ b/examples/SimpleServer/SimpleServer.ino @@ -19,15 +19,62 @@ #include -#include -#include -#include +#if ASYNC_JSON_SUPPORT == 1 + #include + #include + #include +#endif + #include AsyncWebServer server(80); AsyncEventSource events("/events"); AsyncWebSocket ws("/ws"); +///////////////////////////////////////////////////////////////////////////////////////////////////// +// Middlewares +///////////////////////////////////////////////////////////////////////////////////////////////////// + +// log incoming requests +LoggingMiddleware requestLogger; + +// CORS +CorsMiddleware cors; + +// maximum 5 requests per 10 seconds +RateLimitMiddleware rateLimit; + +// filter out specific headers from the incoming request +HeaderFilterMiddleware headerFilter; + +// remove all headers from the incoming request except the ones provided in the constructor +HeaderFreeMiddleware headerFree; + +// basicAuth +AuthenticationMiddleware basicAuth; +AuthenticationMiddleware basicAuthHash; + +// simple digest authentication +AuthenticationMiddleware digestAuth; +AuthenticationMiddleware digestAuthHash; + +// complex authentication which adds request attributes for the next middlewares and handler +AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest* request, ArMiddlewareNext next) { + if (!request->authenticate("user", "password")) { + return request->requestAuthentication(); + } + request->setAttribute("user", "Mathieu"); + request->setAttribute("role", "staff"); + + next(); + + request->getResponse()->addHeader("X-Rate-Limit", "200"); +}); + +AuthorizationMiddleware authz([](AsyncWebServerRequest* request) { return request->getAttribute("role") == "staff"; }); + +///////////////////////////////////////////////////////////////////////////////////////////////////// + const char* PARAM_MESSAGE PROGMEM = "message"; const char* SSE_HTLM PROGMEM = R"( @@ -64,8 +111,10 @@ void notFound(AsyncWebServerRequest* request) { request->send(404, "text/plain", "Not found"); } +#if ASYNC_JSON_SUPPORT == 1 AsyncCallbackJsonWebHandler* jsonHandler = new AsyncCallbackJsonWebHandler("/json2"); AsyncCallbackMessagePackWebHandler* msgPackHandler = new AsyncCallbackMessagePackWebHandler("/msgpack2"); +#endif void setup() { @@ -85,6 +134,171 @@ void setup() { WiFi.softAP("esp-captive"); #endif + // curl -v -X GET http://192.168.4.1/handler-not-sending-response + server.on("/handler-not-sending-response", HTTP_GET, [](AsyncWebServerRequest* request) { + // handler forgot to send a response to the client => 501 Not Implemented + }); + + // This is possible to replace a response. + // the previous one will be deleted. + // response sending happens when the handler returns. + // curl -v -X GET http://192.168.4.1/replace + server.on("/replace", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world"); + // oups! finally we want to send a different response + request->send(400, "text/plain", "validation error"); +#ifndef TARGET_RP2040 + Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); +#endif + }); + + /////////////////////////////////////////////////////////////////////// + // Request header manipulations + /////////////////////////////////////////////////////////////////////// + + // curl -v -X GET -H "x-remove-me: value" http://192.168.4.1/headers + server.on("/headers", HTTP_GET, [](AsyncWebServerRequest* request) { + Serial.printf("Request Headers:\n"); + for (auto& h : request->getHeaders()) + Serial.printf("Request Header: %s = %s\n", h.name().c_str(), h.value().c_str()); + + // remove x-remove-me header + request->removeHeader("x-remove-me"); + Serial.printf("Request Headers:\n"); + for (auto& h : request->getHeaders()) + Serial.printf("Request Header: %s = %s\n", h.name().c_str(), h.value().c_str()); + + std::vector headers; + request->getHeaderNames(headers); + for (auto& h : headers) + Serial.printf("Request Header Name: %s\n", h); + + request->send(200); + }); + + /////////////////////////////////////////////////////////////////////// + // Middlewares at server level (will apply to all requests) + /////////////////////////////////////////////////////////////////////// + + requestLogger.setOutput(Serial); + + basicAuth.setUsername("admin"); + basicAuth.setPassword("admin"); + basicAuth.setRealm("MyApp"); + basicAuth.setAuthFailureMessage("Authentication failed"); + basicAuth.setAuthType(AsyncAuthType::AUTH_BASIC); + basicAuth.generateHash(); + + basicAuthHash.setUsername("admin"); + basicAuthHash.setPasswordHash("YWRtaW46YWRtaW4="); // BASE64(admin:admin) + basicAuthHash.setRealm("MyApp"); + basicAuthHash.setAuthFailureMessage("Authentication failed"); + basicAuthHash.setAuthType(AsyncAuthType::AUTH_BASIC); + + digestAuth.setUsername("admin"); + digestAuth.setPassword("admin"); + digestAuth.setRealm("MyApp"); + digestAuth.setAuthFailureMessage("Authentication failed"); + digestAuth.setAuthType(AsyncAuthType::AUTH_DIGEST); + digestAuth.generateHash(); + + digestAuthHash.setUsername("admin"); + digestAuthHash.setPasswordHash("f499b71f9a36d838b79268e145e132f7"); // MD5(user:realm:pass) + digestAuthHash.setRealm("MyApp"); + digestAuthHash.setAuthFailureMessage("Authentication failed"); + digestAuthHash.setAuthType(AsyncAuthType::AUTH_DIGEST); + + rateLimit.setMaxRequests(5); + rateLimit.setWindowSize(10); + + headerFilter.filter("X-Remove-Me"); + headerFree.keep("X-Keep-Me"); + headerFree.keep("host"); + + // global middleware + server.addMiddleware(&requestLogger); + server.addMiddlewares({&rateLimit, &cors, &headerFilter}); + + cors.setOrigin("http://192.168.4.1"); + cors.setMethods("POST, GET, OPTIONS, DELETE"); + cors.setHeaders("X-Custom-Header"); + cors.setAllowCredentials(false); + cors.setMaxAge(600); + + // Test CORS preflight request + // curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/middleware/cors + server.on("/middleware/cors", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world!"); + }); + + // curl -v -X GET -H "x-remove-me: value" http://192.168.4.1/middleware/test-header-filter + // - requestLogger will log the incoming headers (including x-remove-me) + // - headerFilter will remove x-remove-me header + // - handler will log the remaining headers + server.on("/middleware/test-header-filter", HTTP_GET, [](AsyncWebServerRequest* request) { + for (auto& h : request->getHeaders()) + Serial.printf("Request Header: %s = %s\n", h.name().c_str(), h.value().c_str()); + request->send(200); + }); + + // curl -v -X GET -H "x-keep-me: value" http://192.168.4.1/middleware/test-header-free + // - requestLogger will log the incoming headers (including x-keep-me) + // - headerFree will remove all headers except x-keep-me and host + // - handler will log the remaining headers (x-keep-me and host) + server.on("/middleware/test-header-free", HTTP_GET, [](AsyncWebServerRequest* request) { + for (auto& h : request->getHeaders()) + Serial.printf("Request Header: %s = %s\n", h.name().c_str(), h.value().c_str()); + request->send(200); + }) + .addMiddleware(&headerFree); + + // basic authentication method + // curl -v -X GET -H "origin: http://192.168.4.1" -u admin:admin http://192.168.4.1/middleware/auth-basic + server.on("/middleware/auth-basic", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world!"); + }) + .addMiddleware(&basicAuth); + + // basic authentication method with hash + // curl -v -X GET -H "origin: http://192.168.4.1" -u admin:admin http://192.168.4.1/middleware/auth-basic-hash + server.on("/middleware/auth-basic-hash", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world!"); + }) + .addMiddleware(&basicAuthHash); + + // digest authentication + // curl -v -X GET -H "origin: http://192.168.4.1" -u admin:admin --digest http://192.168.4.1/middleware/auth-digest + server.on("/middleware/auth-digest", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world!"); + }) + .addMiddleware(&digestAuth); + + // digest authentication with hash + // curl -v -X GET -H "origin: http://192.168.4.1" -u admin:admin --digest http://192.168.4.1/middleware/auth-digest-hash + server.on("/middleware/auth-digest-hash", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Hello, world!"); + }) + .addMiddleware(&digestAuthHash); + + // test digest auth with cors + // curl -v -X GET -H "origin: http://192.168.4.1" --digest -u user:password http://192.168.4.1/middleware/auth-custom + server.on("/middleware/auth-custom", HTTP_GET, [](AsyncWebServerRequest* request) { + String buffer = "Hello "; + buffer.concat(request->getAttribute("user")); + buffer.concat(" with role: "); + buffer.concat(request->getAttribute("role")); + request->send(200, "text/plain", buffer); + }) + .addMiddlewares({&complexAuth, &authz}); + + /////////////////////////////////////////////////////////////////////// + + // curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/redirect + // curl -v -X POST -H "origin: http://192.168.4.1" http://192.168.4.1/redirect + server.on("/redirect", HTTP_GET | HTTP_POST, [](AsyncWebServerRequest* request) { + request->redirect("/"); + }); + server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(200, "text/plain", "Hello, world"); }); @@ -139,6 +353,7 @@ void setup() { request->send(200, "text/plain", "Hello, POST: " + message); }); +#if ASYNC_JSON_SUPPORT == 1 // JSON // receives JSON and sends JSON @@ -184,6 +399,7 @@ void setup() { response->setLength(); request->send(response); }); +#endif events.onConnect([](AsyncEventSourceClient* client) { if (client->lastId()) { @@ -197,7 +413,7 @@ void setup() { }); ws.onEvent([](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { - (void) len; + (void)len; if (type == WS_EVT_CONNECT) { Serial.println("ws connect"); client->setCloseClientOnQueueFull(false); @@ -222,8 +438,11 @@ void setup() { server.addHandler(&events); server.addHandler(&ws); + +#if ASYNC_JSON_SUPPORT == 1 server.addHandler(jsonHandler); server.addHandler(msgPackHandler); +#endif server.onNotFound(notFound); @@ -244,6 +463,10 @@ void loop() { } if (now - lastWS >= deltaWS) { ws.printfAll("kp%.4f", (10.0 / 3.0)); + // ws.getClients + for (auto& client : ws.getClients()) { + client.text("kp%.4f", (10.0 / 3.0)); + } lastWS = millis(); } } diff --git a/library.json_ b/library.json_ index db3d040..d228842 100644 --- a/library.json_ +++ b/library.json_ @@ -1,6 +1,6 @@ { "name": "ESPAsyncWebServer", - "version": "3.2.4", + "version": "3.3.7", "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/mathieucarbou/ESPAsyncWebServer", diff --git a/library.properties b/library.properties index bc5ba6d..bb170b9 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ESPAsyncWebServer -version=3.2.4 +version=3.3.7 author=Me-No-Dev maintainer=Mathieu Carbou sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 diff --git a/platformio.ini b/platformio.ini index b72ac65..b0a28f1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,3 +1,13 @@ +[platformio] +default_envs = arduino-2, arduino-3, arduino-310rc1, esp8266, raspberrypi +lib_dir = . +; src_dir = examples/CaptivePortal +src_dir = examples/SimpleServer +; src_dir = examples/StreamFiles +; src_dir = examples/Filters +; src_dir = examples/Draft +; src_dir = examples/issues/Issue14 + [env] framework = arduino build_flags = @@ -13,69 +23,89 @@ build_flags = upload_protocol = esptool monitor_speed = 115200 monitor_filters = esp32_exception_decoder, log2file - -[platformio] -lib_dir = . -; src_dir = examples/CaptivePortal -src_dir = examples/SimpleServer -; src_dir = examples/StreamFiles -; src_dir = examples/Filters -; src_dir = examples/Draft -; src_dir = examples/issues/Issue14 +lib_deps = + ; bblanchon/ArduinoJson @ 5.13.4 + ; bblanchon/ArduinoJson @ 6.21.5 + bblanchon/ArduinoJson @ 7.2.0 + mathieucarbou/AsyncTCP @ 3.2.5 +board = esp32dev [env:arduino-2] -platform = espressif32@6.8.1 -board = esp32dev -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.5 +platform = espressif32@6.9.0 [env:arduino-3] -platform = espressif32 -platform_packages= - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.4 - platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip -board = esp32dev +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip +; board = esp32-s3-devkitc-1 +; board = esp32-c6-devkitc-1 + +[env:arduino-3-no-json] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip +; board = esp32-s3-devkitc-1 +; board = esp32-c6-devkitc-1 lib_deps = - bblanchon/ArduinoJson @ 7.1.0 mathieucarbou/AsyncTCP @ 3.2.5 +[env:arduino-310rc1] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip +; board = esp32-s3-devkitc-1 +; board = esp32-c6-devkitc-1 + [env:esp8266] platform = espressif8266 board = huzzah +; board = d1_mini lib_deps = - bblanchon/ArduinoJson @ 7.1.0 + bblanchon/ArduinoJson @ 7.2.0 esphome/ESPAsyncTCP-esphome @ 2.0.0 ; PlatformIO support for Raspberry Pi Pico is not official ; https://github.com/platformio/platform-raspberrypi/pull/36 ; https://github.com/earlephilhower/arduino-pico/blob/master/docs/platformio.rst ; board settings: https://github.com/earlephilhower/arduino-pico/blob/master/tools/json/rpipico.json -[env:rpipicow] +[env:raspberrypi] upload_protocol = picotool -platform = https://github.com/maxgerhardt/platform-raspberrypi.git +; platform = raspberrypi +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#f2687073f73d554c9db41f29b4769fd9703f4e55 board = rpipicow lib_deps = - bblanchon/ArduinoJson @ 7.1.0 + bblanchon/ArduinoJson @ 7.2.0 khoih-prog/AsyncTCP_RP2040W @ 1.2.0 +build_flags = ${env.build_flags} + -Wno-missing-field-initializers -[env:pioarduino-esp32dev] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -board = esp32dev +; CI + +[env:ci-arduino-2] +platform = espressif32@6.9.0 +board = ${sysenv.PIO_BOARD} + +[env:ci-arduino-3] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip +board = ${sysenv.PIO_BOARD} + +[env:ci-arduino-3-no-json] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip +board = ${sysenv.PIO_BOARD} lib_deps = - bblanchon/ArduinoJson @ 7.1.0 mathieucarbou/AsyncTCP @ 3.2.5 -[env:pioarduino-c6] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -board = esp32-c6-devkitc-1 -lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.5 +[env:ci-arduino-310rc1] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip +board = ${sysenv.PIO_BOARD} -[env:pioarduino-h2] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -board = esp32-h2-devkitm-1 +[env:ci-esp8266] +platform = espressif8266 +board = ${sysenv.PIO_BOARD} lib_deps = - bblanchon/ArduinoJson @ 7.1.0 - mathieucarbou/AsyncTCP @ 3.2.5 + bblanchon/ArduinoJson @ 7.2.0 + esphome/ESPAsyncTCP-esphome @ 2.0.0 + +[env:ci-raspberrypi] +; platform = raspberrypi +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#f2687073f73d554c9db41f29b4769fd9703f4e55 +board = ${sysenv.PIO_BOARD} +lib_deps = + bblanchon/ArduinoJson @ 7.2.0 + khoih-prog/AsyncTCP_RP2040W @ 1.2.0 +build_flags = ${env.build_flags} + -Wno-missing-field-initializers diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 6a290e1..2d6ab7b 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -22,7 +22,6 @@ #include #endif #include "AsyncEventSource.h" -#include "literals.h" using namespace asyncsrv; @@ -209,7 +208,7 @@ void AsyncEventSourceClient::_queueMessage(const char* message, size_t len) { } } -void AsyncEventSourceClient::_onAck(size_t len, uint32_t time) { +void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { #ifdef ESP32 // Same here, acquiring the lock early std::lock_guard lock(_lockmq); @@ -289,7 +288,9 @@ void AsyncEventSource::onConnect(ArEventHandlerFunction cb) { } void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) { - _authorizeConnectHandler = cb; + AuthorizationMiddleware* m = new AuthorizationMiddleware(401, cb); + m->_freeOnRemoval = true; + addMiddleware(m); } void AsyncEventSource::_addClient(AsyncEventSourceClient* client) { @@ -374,20 +375,10 @@ bool AsyncEventSource::canHandle(AsyncWebServerRequest* request) { if (request->method() != HTTP_GET || !request->url().equals(_url)) { return false; } - request->addInterestingHeader(T_Last_Event_ID); - request->addInterestingHeader(T_Cookie); return true; } void AsyncEventSource::handleRequest(AsyncWebServerRequest* request) { - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) { - return request->requestAuthentication(); - } - if (_authorizeConnectHandler != NULL) { - if (!_authorizeConnectHandler(request)) { - return request->send(401); - } - } request->send(new AsyncEventSourceResponse(this)); } diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index 2eae211..9fc4b7a 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -21,7 +21,6 @@ #define ASYNCEVENTSOURCE_H_ #include -#include #ifdef ESP32 #include #include @@ -53,7 +52,7 @@ class AsyncEventSource; class AsyncEventSourceResponse; class AsyncEventSourceClient; using ArEventHandlerFunction = std::function; -using ArAuthorizeConnectHandler = std::function; +using ArAuthorizeConnectHandler = ArAuthorizeFunction; class AsyncEventSourceMessage { private: @@ -116,7 +115,6 @@ class AsyncEventSource : public AsyncWebHandler { mutable std::mutex _client_queue_lock; #endif ArEventHandlerFunction _connectcb{nullptr}; - ArAuthorizeConnectHandler _authorizeConnectHandler; public: AsyncEventSource(const String& url) : _url(url) {}; diff --git a/src/AsyncJson.cpp b/src/AsyncJson.cpp new file mode 100644 index 0000000..7117a70 --- /dev/null +++ b/src/AsyncJson.cpp @@ -0,0 +1,155 @@ +#include "AsyncJson.h" + +#if ASYNC_JSON_SUPPORT == 1 + + #if ARDUINOJSON_VERSION_MAJOR == 5 +AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} { + _code = 200; + _contentType = asyncsrv::T_application_json; + if (isArray) + _root = _jsonBuffer.createArray(); + else + _root = _jsonBuffer.createObject(); +} + #elif ARDUINOJSON_VERSION_MAJOR == 6 +AsyncJsonResponse::AsyncJsonResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { + _code = 200; + _contentType = asyncsrv::T_application_json; + if (isArray) + _root = _jsonBuffer.createNestedArray(); + else + _root = _jsonBuffer.createNestedObject(); +} + #else +AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} { + _code = 200; + _contentType = asyncsrv::T_application_json; + if (isArray) + _root = _jsonBuffer.add(); + else + _root = _jsonBuffer.add(); +} + #endif + +size_t AsyncJsonResponse::setLength() { + #if ARDUINOJSON_VERSION_MAJOR == 5 + _contentLength = _root.measureLength(); + #else + _contentLength = measureJson(_root); + #endif + if (_contentLength) { + _isValid = true; + } + return _contentLength; +} + +size_t AsyncJsonResponse::_fillBuffer(uint8_t* data, size_t len) { + ChunkPrint dest(data, _sentLength, len); + #if ARDUINOJSON_VERSION_MAJOR == 5 + _root.printTo(dest); + #else + serializeJson(_root, dest); + #endif + return len; +} + + #if ARDUINOJSON_VERSION_MAJOR == 6 +PrettyAsyncJsonResponse::PrettyAsyncJsonResponse(bool isArray, size_t maxJsonBufferSize) : AsyncJsonResponse{isArray, maxJsonBufferSize} {} + #else +PrettyAsyncJsonResponse::PrettyAsyncJsonResponse(bool isArray) : AsyncJsonResponse{isArray} {} + #endif + +size_t PrettyAsyncJsonResponse::setLength() { + #if ARDUINOJSON_VERSION_MAJOR == 5 + _contentLength = _root.measurePrettyLength(); + #else + _contentLength = measureJsonPretty(_root); + #endif + if (_contentLength) { + _isValid = true; + } + return _contentLength; +} + +size_t PrettyAsyncJsonResponse::_fillBuffer(uint8_t* data, size_t len) { + ChunkPrint dest(data, _sentLength, len); + #if ARDUINOJSON_VERSION_MAJOR == 5 + _root.prettyPrintTo(dest); + #else + serializeJsonPretty(_root, dest); + #endif + return len; +} + + #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) {} + #else +AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest) + : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} + #endif + +bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest* request) { + if (!_onRequest) + return false; + + WebRequestMethodComposite request_method = request->method(); + if (!(_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_json)) + return false; + + return true; +} + +void AsyncCallbackJsonWebHandler::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 == 5 + DynamicJsonBuffer jsonBuffer; + JsonVariant json = jsonBuffer.parse((uint8_t*)(request->_tempObject)); + if (json.success()) { + #elif ARDUINOJSON_VERSION_MAJOR == 6 + DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); + DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as(); + #else + JsonDocument jsonBuffer; + DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as(); + #endif + + _onRequest(request, json); + return; + } + } + request->send(_contentLength > _maxContentLength ? 413 : 400); + } else { + request->send(500); + } +} + +void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) { + if (_onRequest) { + _contentLength = total; + if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { + request->_tempObject = malloc(total); + } + if (request->_tempObject != NULL) { + memcpy((uint8_t*)(request->_tempObject) + index, data, len); + } + } +} + +#endif // ASYNC_JSON_SUPPORT diff --git a/src/AsyncJson.h b/src/AsyncJson.h index bca3f24..167364a 100644 --- a/src/AsyncJson.h +++ b/src/AsyncJson.h @@ -34,222 +34,98 @@ */ #ifndef ASYNC_JSON_H_ #define ASYNC_JSON_H_ -#include -#include -#include "ChunkPrint.h" +#if __has_include("ArduinoJson.h") + #include + #if ARDUINOJSON_VERSION_MAJOR >= 5 + #define ASYNC_JSON_SUPPORT 1 + #else + #define ASYNC_JSON_SUPPORT 0 + #endif // ARDUINOJSON_VERSION_MAJOR >= 5 +#endif // __has_include("ArduinoJson.h") -#if ARDUINOJSON_VERSION_MAJOR == 6 - #ifndef DYNAMIC_JSON_DOCUMENT_SIZE - #define DYNAMIC_JSON_DOCUMENT_SIZE 1024 +#if ASYNC_JSON_SUPPORT == 1 + #include + + #include "ChunkPrint.h" + + #if ARDUINOJSON_VERSION_MAJOR == 6 + #ifndef DYNAMIC_JSON_DOCUMENT_SIZE + #define DYNAMIC_JSON_DOCUMENT_SIZE 1024 + #endif #endif -#endif - -constexpr const char* JSON_MIMETYPE = "application/json"; - -/* - * Json Response - * */ class AsyncJsonResponse : public AsyncAbstractResponse { protected: -#if ARDUINOJSON_VERSION_MAJOR == 5 + #if ARDUINOJSON_VERSION_MAJOR == 5 DynamicJsonBuffer _jsonBuffer; -#elif ARDUINOJSON_VERSION_MAJOR == 6 + #elif ARDUINOJSON_VERSION_MAJOR == 6 DynamicJsonDocument _jsonBuffer; -#else + #else JsonDocument _jsonBuffer; -#endif + #endif JsonVariant _root; bool _isValid; public: -#if ARDUINOJSON_VERSION_MAJOR == 5 - AsyncJsonResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.createArray(); - else - _root = _jsonBuffer.createObject(); - } -#elif ARDUINOJSON_VERSION_MAJOR == 6 - AsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.createNestedArray(); - else - _root = _jsonBuffer.createNestedObject(); - } -#else - AsyncJsonResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = JSON_MIMETYPE; - if (isArray) - _root = _jsonBuffer.add(); - else - _root = _jsonBuffer.add(); - } -#endif - + #if ARDUINOJSON_VERSION_MAJOR == 6 + AsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + #else + AsyncJsonResponse(bool isArray = false); + #endif JsonVariant& getRoot() { return _root; } bool _sourceValid() const { return _isValid; } - size_t setLength() { - -#if ARDUINOJSON_VERSION_MAJOR == 5 - _contentLength = _root.measureLength(); -#else - _contentLength = measureJson(_root); -#endif - - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - + size_t setLength(); size_t getSize() const { return _jsonBuffer.size(); } - -#if ARDUINOJSON_VERSION_MAJOR >= 6 + size_t _fillBuffer(uint8_t* data, size_t len); + #if ARDUINOJSON_VERSION_MAJOR >= 6 bool overflowed() const { return _jsonBuffer.overflowed(); } -#endif - - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); - -#if ARDUINOJSON_VERSION_MAJOR == 5 - _root.printTo(dest); -#else - serializeJson(_root, dest); -#endif - return len; - } + #endif }; class PrettyAsyncJsonResponse : public AsyncJsonResponse { public: -#if ARDUINOJSON_VERSION_MAJOR == 6 - PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse{isArray, maxJsonBufferSize} {} -#else - PrettyAsyncJsonResponse(bool isArray = false) : AsyncJsonResponse{isArray} {} -#endif - size_t setLength() { -#if ARDUINOJSON_VERSION_MAJOR == 5 - _contentLength = _root.measurePrettyLength(); -#else - _contentLength = measureJsonPretty(_root); -#endif - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); -#if ARDUINOJSON_VERSION_MAJOR == 5 - _root.prettyPrintTo(dest); -#else - serializeJsonPretty(_root, dest); -#endif - return len; - } + #if ARDUINOJSON_VERSION_MAJOR == 6 + PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + #else + PrettyAsyncJsonResponse(bool isArray = false); + #endif + size_t setLength(); + size_t _fillBuffer(uint8_t* data, size_t len); }; typedef std::function ArJsonRequestHandlerFunction; class AsyncCallbackJsonWebHandler : public AsyncWebHandler { - private: protected: - const String _uri; + String _uri; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; size_t _contentLength; -#if ARDUINOJSON_VERSION_MAJOR == 6 - const size_t maxJsonBufferSize; -#endif + #if ARDUINOJSON_VERSION_MAJOR == 6 + size_t maxJsonBufferSize; + #endif size_t _maxContentLength; public: -#if ARDUINOJSON_VERSION_MAJOR == 6 - AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {} -#else - AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} -#endif + #if ARDUINOJSON_VERSION_MAJOR == 6 + AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + #else + AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr); + #endif void setMethod(WebRequestMethodComposite method) { _method = method; } void setMaxContentLength(int maxContentLength) { _maxContentLength = maxContentLength; } void onRequest(ArJsonRequestHandlerFunction fn) { _onRequest = fn; } - virtual bool canHandle(AsyncWebServerRequest* request) override final { - if (!_onRequest) - return false; - - WebRequestMethodComposite request_method = request->method(); - if (!(_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(JSON_MIMETYPE)) - return false; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onRequest) { - if (request->method() == HTTP_GET) { - JsonVariant json; - _onRequest(request, json); - return; - } else if (request->_tempObject != NULL) { - -#if ARDUINOJSON_VERSION_MAJOR == 5 - DynamicJsonBuffer jsonBuffer; - JsonVariant json = jsonBuffer.parse((uint8_t*)(request->_tempObject)); - if (json.success()) { -#elif ARDUINOJSON_VERSION_MAJOR == 6 - DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); -#else - JsonDocument jsonBuffer; - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t*)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); -#endif - - _onRequest(request, json); - return; - } - } - request->send(_contentLength > _maxContentLength ? 413 : 400); - } else { - request->send(500); - } - } - virtual 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 { - } - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); - } - if (request->_tempObject != NULL) { - memcpy((uint8_t*)(request->_tempObject) + index, data, len); - } - } - } - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } + virtual bool canHandle(AsyncWebServerRequest* request) override final; + virtual void handleRequest(AsyncWebServerRequest* request) override final; + virtual 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 {} + virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final; + virtual bool isRequestHandlerTrivial() override final { return !_onRequest; } }; -#endif + +#endif // ASYNC_JSON_SUPPORT == 1 + +#endif // ASYNC_JSON_H_ diff --git a/src/AsyncMessagePack.cpp b/src/AsyncMessagePack.cpp new file mode 100644 index 0000000..076f558 --- /dev/null +++ b/src/AsyncMessagePack.cpp @@ -0,0 +1,106 @@ +#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(); + else + _root = _jsonBuffer.add(); +} + #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) { + if (!_onRequest) + return false; + + WebRequestMethodComposite request_method = request->method(); + if (!(_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(); + #else + JsonDocument jsonBuffer; + DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t*)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as(); + #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) { + memcpy((uint8_t*)(request->_tempObject) + index, data, len); + } + } +} + +#endif // ASYNC_MSG_PACK_SUPPORT diff --git a/src/AsyncMessagePack.h b/src/AsyncMessagePack.h index 57a8824..9ac5ee5 100644 --- a/src/AsyncMessagePack.h +++ b/src/AsyncMessagePack.h @@ -21,125 +21,82 @@ server.addHandler(handler); */ -#include -#include +#if __has_include("ArduinoJson.h") + #include + #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") -#include "ChunkPrint.h" -#include "literals.h" +#if ASYNC_MSG_PACK_SUPPORT == 1 + #include + + #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: - AsyncMessagePackResponse(bool isArray = false) : _isValid{false} { - _code = 200; - _contentType = asyncsrv::T_application_msgpack; - if (isArray) - _root = _jsonBuffer.add(); - else - _root = _jsonBuffer.add(); - } - + #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() { - _contentLength = measureMsgPack(_root); - if (_contentLength) { - _isValid = true; - } - return _contentLength; - } - + size_t setLength(); size_t getSize() const { return _jsonBuffer.size(); } - - size_t _fillBuffer(uint8_t* data, size_t len) { - ChunkPrint dest(data, _sentLength, len); - serializeMsgPack(_root, dest); - return len; - } + size_t _fillBuffer(uint8_t* data, size_t len); + #if ARDUINOJSON_VERSION_MAJOR >= 6 + bool overflowed() const { return _jsonBuffer.overflowed(); } + #endif }; -class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler { - public: - typedef std::function ArJsonRequestHandlerFunction; +typedef std::function ArMessagePackRequestHandlerFunction; +class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler { protected: - const String _uri; + String _uri; WebRequestMethodComposite _method; - ArJsonRequestHandlerFunction _onRequest; + ArMessagePackRequestHandlerFunction _onRequest; size_t _contentLength; + #if ARDUINOJSON_VERSION_MAJOR == 6 + size_t maxJsonBufferSize; + #endif size_t _maxContentLength; public: - AsyncCallbackMessagePackWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest = nullptr) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} + #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(ArJsonRequestHandlerFunction fn) { _onRequest = fn; } - - virtual bool canHandle(AsyncWebServerRequest* request) override final { - if (!_onRequest) - return false; - - WebRequestMethodComposite request_method = request->method(); - if (!(_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; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - - if (_onRequest) { - if (request->method() == HTTP_GET) { - JsonVariant json; - _onRequest(request, json); - return; - - } else if (request->_tempObject != NULL) { - JsonDocument jsonBuffer; - DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t*)(request->_tempObject)); - - if (!error) { - JsonVariant json = jsonBuffer.as(); - _onRequest(request, json); - return; - } - } - request->send(_contentLength > _maxContentLength ? 413 : 400); - } else { - request->send(500); - } - } + void onRequest(ArMessagePackRequestHandlerFunction fn) { _onRequest = fn; } + virtual bool canHandle(AsyncWebServerRequest* request) override final; + virtual void handleRequest(AsyncWebServerRequest* request) override final; virtual 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 {} - - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); - } - if (request->_tempObject != NULL) { - memcpy((uint8_t*)(request->_tempObject) + index, data, len); - } - } - } - - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } + virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final; + virtual bool isRequestHandlerTrivial() override final { return !_onRequest; } }; + +#endif // ASYNC_MSG_PACK_SUPPORT == 1 diff --git a/src/AsyncWebHeader.cpp b/src/AsyncWebHeader.cpp new file mode 100644 index 0000000..ba271a3 --- /dev/null +++ b/src/AsyncWebHeader.cpp @@ -0,0 +1,22 @@ +#include + +AsyncWebHeader::AsyncWebHeader(const String& data) { + if (!data) + return; + int index = data.indexOf(':'); + if (index < 0) + return; + _name = data.substring(0, index); + _value = data.substring(index + 2); +} + +String AsyncWebHeader::toString() const { + String str; + str.reserve(_name.length() + _value.length() + 2); + str.concat(_name); + str.concat((char)0x3a); + str.concat((char)0x20); + str.concat(_value); + str.concat(asyncsrv::T_rn); + return str; +} diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 0b87cef..b040a9f 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -381,7 +381,7 @@ size_t AsyncWebSocketClient::queueLen() const { std::lock_guard lock(_lock); #endif - return _messageQueue.size() + _controlQueue.size(); + return _messageQueue.size(); } bool AsyncWebSocketClient::canSend() const { @@ -1078,13 +1078,6 @@ bool AsyncWebSocket::canHandle(AsyncWebServerRequest* request) { if (request->method() != HTTP_GET || !request->url().equals(_url) || !request->isExpectedRequestedConnType(RCT_WS)) return false; - request->addInterestingHeader(WS_STR_CONNECTION); - request->addInterestingHeader(WS_STR_UPGRADE); - request->addInterestingHeader(WS_STR_ORIGIN); - request->addInterestingHeader(WS_STR_COOKIE); - request->addInterestingHeader(WS_STR_VERSION); - request->addInterestingHeader(WS_STR_KEY); - request->addInterestingHeader(WS_STR_PROTOCOL); return true; } @@ -1093,9 +1086,6 @@ void AsyncWebSocket::handleRequest(AsyncWebServerRequest* request) { request->send(400); return; } - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) { - return request->requestAuthentication(); - } if (_handshakeHandler != nullptr) { if (!_handshakeHandler(request)) { request->send(401); diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 34256a7..1da3d15 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -42,8 +42,6 @@ #include -#include -#include #include #ifdef ESP8266 @@ -281,7 +279,7 @@ class AsyncWebSocket : public AsyncWebHandler { public: explicit AsyncWebSocket(const char* url) : _url(url), _cNextId(1), _enabled(true) {} AsyncWebSocket(const String& url) : _url(url), _cNextId(1), _enabled(true) {} - ~AsyncWebSocket(){}; + ~AsyncWebSocket() {}; const char* url() const { return _url.c_str(); } void enable(bool e) { _enabled = e; } bool enabled() const { return _enabled; } @@ -339,15 +337,8 @@ class AsyncWebSocket : public AsyncWebHandler { size_t printfAll_P(PGM_P formatP, ...) __attribute__((format(printf, 2, 3))); #endif - // event listener - void onEvent(AwsEventHandler handler) { - _eventHandler = handler; - } - - // Handshake Handler - void handleHandshake(AwsHandshakeHandler handler) { - _handshakeHandler = handler; - } + void onEvent(AwsEventHandler handler) { _eventHandler = handler; } + void handleHandshake(AwsHandshakeHandler handler) { _handshakeHandler = handler; } // system callbacks (do not call) uint32_t _getNextId() { return _cNextId++; } @@ -360,7 +351,7 @@ class AsyncWebSocket : public AsyncWebHandler { AsyncWebSocketMessageBuffer* makeBuffer(size_t size = 0); AsyncWebSocketMessageBuffer* makeBuffer(const uint8_t* data, size_t size); - const std::list& getClients() const { return _clients; } + std::list& getClients() { return _clients; } }; // WebServer response to authenticate the socket and detach the tcp client from the web server request diff --git a/src/ChunkPrint.cpp b/src/ChunkPrint.cpp new file mode 100644 index 0000000..8c9717a --- /dev/null +++ b/src/ChunkPrint.cpp @@ -0,0 +1,16 @@ +#include + +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; + return 1; + } + return 0; +} \ No newline at end of file diff --git a/src/ChunkPrint.h b/src/ChunkPrint.h index 2f40741..103d21e 100644 --- a/src/ChunkPrint.h +++ b/src/ChunkPrint.h @@ -11,22 +11,9 @@ class ChunkPrint : public Print { size_t _pos; public: - ChunkPrint(uint8_t* destination, size_t from, size_t len) - : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {} + ChunkPrint(uint8_t* destination, size_t from, size_t len); virtual ~ChunkPrint() {} - size_t write(uint8_t c) { - if (_to_skip > 0) { - _to_skip--; - return 1; - } else if (_to_write > 0) { - _to_write--; - _destination[_pos++] = c; - return 1; - } - return 0; - } - size_t write(const uint8_t* buffer, size_t size) { - return this->Print::write(buffer, size); - } + size_t write(uint8_t c); + size_t write(const uint8_t* buffer, size_t size) { return this->Print::write(buffer, size); } }; #endif diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index df578f5..dfaef9c 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -24,8 +24,11 @@ #include "Arduino.h" #include "FS.h" +#include +#include #include #include +#include #include #ifdef ESP32 @@ -45,10 +48,10 @@ #include "literals.h" -#define ASYNCWEBSERVER_VERSION "3.2.4" +#define ASYNCWEBSERVER_VERSION "3.3.7" #define ASYNCWEBSERVER_VERSION_MAJOR 3 -#define ASYNCWEBSERVER_VERSION_MINOR 2 -#define ASYNCWEBSERVER_VERSION_REVISION 4 +#define ASYNCWEBSERVER_VERSION_MINOR 3 +#define ASYNCWEBSERVER_VERSION_REVISION 7 #define ASYNCWEBSERVER_FORK_mathieucarbou #ifdef ASYNCWEBSERVER_REGEX @@ -67,6 +70,7 @@ class AsyncWebHandler; class AsyncStaticWebHandler; class AsyncCallbackWebHandler; class AsyncResponseStream; +class AsyncMiddlewareChain; #if defined(TARGET_RP2040) typedef enum http_method WebRequestMethod; @@ -99,7 +103,8 @@ namespace fs { #endif // if this value is returned when asked for data, packet will not be sent and you will be asked for data again -#define RESPONSE_TRY_AGAIN 0xFFFFFFFF +#define RESPONSE_TRY_AGAIN 0xFFFFFFFF +#define RESPONSE_STREAM_BUFFER_SIZE 1460 typedef uint8_t WebRequestMethodComposite; typedef std::function ArDisconnectHandler; @@ -137,33 +142,15 @@ class AsyncWebHeader { public: AsyncWebHeader() = default; AsyncWebHeader(const AsyncWebHeader&) = default; - AsyncWebHeader(const char* name, const char* value) : _name(name), _value(value) {} AsyncWebHeader(const String& name, const String& value) : _name(name), _value(value) {} - AsyncWebHeader(const String& data) { - if (!data) - return; - int index = data.indexOf(':'); - if (index < 0) - return; - _name = data.substring(0, index); - _value = data.substring(index + 2); - } + AsyncWebHeader(const String& data); AsyncWebHeader& operator=(const AsyncWebHeader&) = default; const String& name() const { return _name; } const String& value() const { return _value; } - String toString() const { - String str; - str.reserve(_name.length() + _value.length() + 2); - str.concat(_name); - str.concat((char)0x3a); - str.concat((char)0x20); - str.concat(_value); - str.concat(asyncsrv::T_rn); - return str; - } + String toString() const; }; /* @@ -177,6 +164,15 @@ typedef enum { RCT_NOT_USED = -1, RCT_EVENT, RCT_MAX } RequestedConnectionType; +// this enum is similar to Arduino WebServer's AsyncAuthType and PsychicHttp +typedef enum { + AUTH_NONE = 0, + AUTH_BASIC, + AUTH_DIGEST, + AUTH_BEARER, + AUTH_OTHER, +} AsyncAuthType; + typedef std::function AwsResponseFiller; typedef std::function AwsTemplateProcessor; @@ -191,9 +187,11 @@ class AsyncWebServerRequest { AsyncWebServer* _server; AsyncWebHandler* _handler; AsyncWebServerResponse* _response; - std::vector _interestingHeaders; ArDisconnectHandler _onDisconnectfn; + // response is sent + bool _sent = false; + String _temp; uint8_t _parseState; @@ -205,8 +203,7 @@ class AsyncWebServerRequest { String _boundary; String _authorization; RequestedConnectionType _reqconntype; - void _removeNotInterestingHeaders(); - bool _isDigest; + AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE; bool _isMultipart; bool _isPlainPost; bool _expectingContinue; @@ -217,6 +214,8 @@ class AsyncWebServerRequest { std::list _params; std::vector _pathParams; + std::unordered_map, std::equal_to> _attributes; + uint8_t _multiParseState; uint8_t _boundaryPosition; size_t _itemStartIndex; @@ -281,31 +280,38 @@ class AsyncWebServerRequest { // base64(user:pass) for basic or // user:realm:md5(user:realm:pass) for digest bool authenticate(const char* hash); - bool authenticate(const char* username, const char* password, const char* realm = NULL, bool passwordIsHash = false); - void requestAuthentication(const char* realm = NULL, bool isDigest = true); + bool authenticate(const char* username, const char* credentials, const char* realm = NULL, bool isHash = false); + void requestAuthentication(const char* realm = nullptr, bool isDigest = true) { requestAuthentication(isDigest ? AsyncAuthType::AUTH_DIGEST : AsyncAuthType::AUTH_BASIC, realm); } + void requestAuthentication(AsyncAuthType method, const char* realm = nullptr, const char* _authFailMsg = nullptr); void setHandler(AsyncWebHandler* handler) { _handler = handler; } - /** - * @brief add header to collect from a response - * - * @param name - */ - void addInterestingHeader(const char* name); - void addInterestingHeader(const String& name) { return addInterestingHeader(name.c_str()); }; +#ifndef ESP8266 + [[deprecated("All headers are now collected. Use removeHeader(name) or HeaderFreeMiddleware if you really need to free some headers.")]] +#endif + void addInterestingHeader(__unused const char* name) { + } +#ifndef ESP8266 + [[deprecated("All headers are now collected. Use removeHeader(name) or HeaderFreeMiddleware if you really need to free some headers.")]] +#endif + void addInterestingHeader(__unused const String& name) { + } /** - * @brief issue 302 redirect response + * @brief issue HTTP redirect responce with Location header * - * @param url + * @param url - url to redirect to + * @param code - responce code, default is 302 : temporary redirect */ - void redirect(const char* url); - void redirect(const String& url) { return redirect(url.c_str()); }; + void redirect(const char* url, int code = 302); + void redirect(const String& url, int code = 302) { return redirect(url.c_str(), code); }; void send(AsyncWebServerResponse* response); + AsyncWebServerResponse* getResponse() const { return _response; } void send(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } - void send(int code, const String& contentType, const String& content = emptyString, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } + void send(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType.c_str(), content, callback)); } + void send(int code, const String& contentType, const String& content, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType.c_str(), content.c_str(), callback)); } void send(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, len, callback)); } void send(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, len, callback)); } @@ -336,24 +342,25 @@ class AsyncWebServerRequest { void sendChunked(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { send(beginChunkedResponse(contentType, callback, templateCallback)); } #ifndef ESP8266 - [[deprecated("Replaced by send(...)")]] + [[deprecated("Replaced by send(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr)")]] #endif void send_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { send(code, contentType, content, len, callback); } #ifndef ESP8266 - [[deprecated("Replaced by send(...)")]] -#endif + [[deprecated("Replaced by send(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr)")]] void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { send(code, contentType, content, callback); } - -#ifdef ESP8266 - void send(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { send(beginResponse(code, contentType, content, callback)); } +#else + void send_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { + send(beginResponse_P(code, contentType, content, callback)); + } #endif AsyncWebServerResponse* beginResponse(int code, const char* contentType = asyncsrv::empty, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr); - AsyncWebServerResponse* beginResponse(int code, const String& contentType, const String& content = emptyString, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content.c_str(), callback); } + AsyncWebServerResponse* beginResponse(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, callback); } + AsyncWebServerResponse* beginResponse(int code, const String& contentType, const String& content, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content.c_str(), callback); } AsyncWebServerResponse* beginResponse(int code, const char* contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr); AsyncWebServerResponse* beginResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, len, callback); } @@ -373,48 +380,19 @@ class AsyncWebServerRequest { AsyncWebServerResponse* beginChunkedResponse(const char* contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); AsyncWebServerResponse* beginChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); - AsyncResponseStream* beginResponseStream(const char* contentType, size_t bufferSize = 1460); - AsyncResponseStream* beginResponseStream(const String& contentType, size_t bufferSize = 1460) { return beginResponseStream(contentType.c_str(), bufferSize); } + AsyncResponseStream* beginResponseStream(const char* contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE); + AsyncResponseStream* beginResponseStream(const String& contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) { return beginResponseStream(contentType.c_str(), bufferSize); } #ifndef ESP8266 - [[deprecated("Replaced by beginResponse(...)")]] + [[deprecated("Replaced by beginResponse(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr)")]] #endif AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, const uint8_t* content, size_t len, AwsTemplateProcessor callback = nullptr) { return beginResponse(code, contentType.c_str(), content, len, callback); } #ifndef ESP8266 - [[deprecated("Replaced by beginResponse(...)")]] -#endif - AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr) { - return beginResponse(code, contentType.c_str(), content, callback); - } - -#ifdef ESP8266 - AsyncWebServerResponse* beginResponse(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); -#endif - - size_t headers() const; // get header count - - // check if header exists - bool hasHeader(const char* name) const; - bool hasHeader(const String& name) const { return hasHeader(name.c_str()); }; -#ifdef ESP8266 - bool hasHeader(const __FlashStringHelper* data) const; // check if header exists -#endif - - const AsyncWebHeader* getHeader(const char* name) const; - const AsyncWebHeader* getHeader(const String& name) const { return getHeader(name.c_str()); }; -#ifdef ESP8266 - const AsyncWebHeader* getHeader(const __FlashStringHelper* data) const; -#endif - const AsyncWebHeader* getHeader(size_t num) const; - - size_t params() const; // get arguments count - bool hasParam(const char* name, bool post = false, bool file = false) const; - bool hasParam(const String& name, bool post = false, bool file = false) const { return hasParam(name.c_str(), post, file); }; -#ifdef ESP8266 - bool hasParam(const __FlashStringHelper* data, bool post = false, bool file = false) const { return hasParam(String(data).c_str(), post, file); }; + [[deprecated("Replaced by beginResponse(int code, const String& contentType, const char* content = asyncsrv::empty, AwsTemplateProcessor callback = nullptr)")]] #endif + AsyncWebServerResponse* beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); /** * @brief Get the Request parameter by name @@ -469,6 +447,56 @@ class AsyncWebServerRequest { const String& header(size_t i) const; // get request header value by number const String& headerName(size_t i) const; // get request header name by number + size_t headers() const; // get header count + + // check if header exists + bool hasHeader(const char* name) const; + bool hasHeader(const String& name) const { return hasHeader(name.c_str()); }; +#ifdef ESP8266 + bool hasHeader(const __FlashStringHelper* data) const; // check if header exists +#endif + + const AsyncWebHeader* getHeader(const char* name) const; + const AsyncWebHeader* getHeader(const String& name) const { return getHeader(name.c_str()); }; +#ifdef ESP8266 + const AsyncWebHeader* getHeader(const __FlashStringHelper* data) const; +#endif + + const AsyncWebHeader* getHeader(size_t num) const; + + const std::list& getHeaders() const { return _headers; } + + size_t getHeaderNames(std::vector& names) const; + + // Remove a header from the request. + // It will free the memory and prevent the header to be seen during request processing. + bool removeHeader(const char* name); + // Remove all request headers. + void removeHeaders() { _headers.clear(); } + + size_t params() const; // get arguments count + bool hasParam(const char* name, bool post = false, bool file = false) const; + bool hasParam(const String& name, bool post = false, bool file = false) const { return hasParam(name.c_str(), post, file); }; +#ifdef ESP8266 + bool hasParam(const __FlashStringHelper* data, bool post = false, bool file = false) const { return hasParam(String(data).c_str(), post, file); }; +#endif + + // REQUEST ATTRIBUTES + + void setAttribute(const char* name, const char* value) { _attributes[name] = value; } + void setAttribute(const char* name, bool value) { _attributes[name] = value ? "1" : emptyString; } + void setAttribute(const char* name, long value) { _attributes[name] = String(value); } + void setAttribute(const char* name, float value, unsigned int decimalPlaces = 2) { _attributes[name] = String(value, decimalPlaces); } + void setAttribute(const char* name, double value, unsigned int decimalPlaces = 2) { _attributes[name] = String(value, decimalPlaces); } + + bool hasAttribute(const char* name) const { return _attributes.find(name) != _attributes.end(); } + + const String& getAttribute(const char* name, const String& defaultValue = emptyString) const; + bool getAttribute(const char* name, bool defaultValue) const; + long getAttribute(const char* name, long defaultValue) const; + float getAttribute(const char* name, float defaultValue) const; + double getAttribute(const char* name, double defaultValue) const; + String urlDecode(const String& text) const; }; @@ -482,6 +510,176 @@ bool ON_STA_FILTER(AsyncWebServerRequest* request); bool ON_AP_FILTER(AsyncWebServerRequest* request); +/* + * MIDDLEWARE :: Request interceptor, assigned to a AsyncWebHandler (or the server), which can be used: + * 1. to run some code before the final handler is executed (e.g. check authentication) + * 2. decide whether to proceed or not with the next handler + * */ + +using ArMiddlewareNext = std::function; +using ArMiddlewareCallback = std::function; + +// Middleware is a base class for all middleware +class AsyncMiddleware { + public: + virtual ~AsyncMiddleware() {} + virtual void run(__unused AsyncWebServerRequest* request, __unused ArMiddlewareNext next) { + return next(); + }; + + private: + friend class AsyncWebHandler; + friend class AsyncEventSource; + friend class AsyncMiddlewareChain; + bool _freeOnRemoval = false; +}; + +// Create a custom middleware by providing an anonymous callback function +class AsyncMiddlewareFunction : public AsyncMiddleware { + public: + AsyncMiddlewareFunction(ArMiddlewareCallback fn) : _fn(fn) {} + void run(AsyncWebServerRequest* request, ArMiddlewareNext next) override { return _fn(request, next); }; + + private: + ArMiddlewareCallback _fn; +}; + +// For internal use only: super class to add/remove middleware to server or handlers +class AsyncMiddlewareChain { + public: + virtual ~AsyncMiddlewareChain(); + + void addMiddleware(ArMiddlewareCallback fn); + void addMiddleware(AsyncMiddleware* middleware); + void addMiddlewares(std::vector middlewares); + bool removeMiddleware(AsyncMiddleware* middleware); + + // For internal use only + void _runChain(AsyncWebServerRequest* request, ArMiddlewareNext finalizer); + + protected: + std::list _middlewares; +}; + +// AuthenticationMiddleware is a middleware that checks if the request is authenticated +class AuthenticationMiddleware : public AsyncMiddleware { + public: + void setUsername(const char* username); + void setPassword(const char* password); + void setPasswordHash(const char* hash); + + void setRealm(const char* realm) { _realm = realm; } + void setAuthFailureMessage(const char* message) { _authFailMsg = message; } + void setAuthType(AsyncAuthType authMethod) { _authMethod = authMethod; } + + // precompute and store the hash value based on the username, realm, and authMethod + // returns true if the hash was successfully generated and replaced + bool generateHash(); + + bool allowed(AsyncWebServerRequest* request); + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + String _username; + String _credentials; + bool _hash = false; + + String _realm = asyncsrv::T_LOGIN_REQ; + AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE; + String _authFailMsg; + bool _hasCreds = false; +}; + +using ArAuthorizeFunction = std::function; +// AuthorizationMiddleware is a middleware that checks if the request is authorized +class AuthorizationMiddleware : public AsyncMiddleware { + public: + AuthorizationMiddleware(ArAuthorizeFunction authorizeConnectHandler) : _code(403), _authz(authorizeConnectHandler) {} + AuthorizationMiddleware(int code, ArAuthorizeFunction authorizeConnectHandler) : _code(code), _authz(authorizeConnectHandler) {} + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next) { return _authz && !_authz(request) ? request->send(_code) : next(); } + + private: + int _code; + ArAuthorizeFunction _authz; +}; + +// remove all headers from the incoming request except the ones provided in the constructor +class HeaderFreeMiddleware : public AsyncMiddleware { + public: + void keep(const char* name) { _toKeep.push_back(name); } + void unKeep(const char* name) { _toKeep.erase(std::remove(_toKeep.begin(), _toKeep.end(), name), _toKeep.end()); } + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + std::vector _toKeep; +}; + +// filter out specific headers from the incoming request +class HeaderFilterMiddleware : public AsyncMiddleware { + public: + void filter(const char* name) { _toRemove.push_back(name); } + void unFilter(const char* name) { _toRemove.erase(std::remove(_toRemove.begin(), _toRemove.end(), name), _toRemove.end()); } + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + std::vector _toRemove; +}; + +// curl-like logging of incoming requests +class LoggingMiddleware : public AsyncMiddleware { + public: + void setOutput(Print& output) { _out = &output; } + void setEnabled(bool enabled) { _enabled = enabled; } + bool isEnabled() { return _enabled && _out; } + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + Print* _out = nullptr; + bool _enabled = true; +}; + +// CORS Middleware +class CorsMiddleware : public AsyncMiddleware { + public: + void setOrigin(const char* origin) { _origin = origin; } + void setMethods(const char* methods) { _methods = methods; } + void setHeaders(const char* headers) { _headers = headers; } + void setAllowCredentials(bool credentials) { _credentials = credentials; } + void setMaxAge(uint32_t seconds) { _maxAge = seconds; } + + void addCORSHeaders(AsyncWebServerResponse* response); + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + String _origin = "*"; + String _methods = "*"; + String _headers = "*"; + bool _credentials = true; + uint32_t _maxAge = 86400; +}; + +// Rate limit Middleware +class RateLimitMiddleware : public AsyncMiddleware { + public: + void setMaxRequests(size_t maxRequests) { _maxRequests = maxRequests; } + void setWindowSize(uint32_t seconds) { _windowSizeMillis = seconds * 1000; } + + bool isRequestAllowed(uint32_t& retryAfterSeconds); + + void run(AsyncWebServerRequest* request, ArMiddlewareNext next); + + private: + size_t _maxRequests = 0; + uint32_t _windowSizeMillis = 0; + std::list _requestTimes; +}; + /* * REWRITE :: One instance can be handle any Request (done by the Server) * */ @@ -517,36 +715,24 @@ class AsyncWebRewrite { * HANDLER :: One instance can be attached to any Request (done by the Server) * */ -class AsyncWebHandler { +class AsyncWebHandler : public AsyncMiddlewareChain { protected: - ArRequestFilterFunction _filter{nullptr}; - String _username; - String _password; + ArRequestFilterFunction _filter = nullptr; + AuthenticationMiddleware* _authMiddleware = nullptr; public: AsyncWebHandler() {} - AsyncWebHandler& setFilter(ArRequestFilterFunction fn) { - _filter = fn; - return *this; - } - AsyncWebHandler& setAuthentication(const char* username, const char* password) { - _username = username; - _password = password; - return *this; - }; - AsyncWebHandler& setAuthentication(const String& username, const String& password) { - _username = username; - _password = password; - return *this; - }; + AsyncWebHandler& setFilter(ArRequestFilterFunction fn); + AsyncWebHandler& setAuthentication(const char* username, const char* password); + AsyncWebHandler& setAuthentication(const String& username, const String& password) { return setAuthentication(username.c_str(), password.c_str()); }; bool filter(AsyncWebServerRequest* request) { return _filter == NULL || _filter(request); } virtual ~AsyncWebHandler() {} virtual bool canHandle(AsyncWebServerRequest* request __attribute__((unused))) { return false; } - virtual void handleRequest(AsyncWebServerRequest* request __attribute__((unused))) {} - virtual void handleUpload(AsyncWebServerRequest* request __attribute__((unused)), const String& filename __attribute__((unused)), size_t index __attribute__((unused)), uint8_t* data __attribute__((unused)), size_t len __attribute__((unused)), bool final __attribute__((unused))) {} - virtual void handleBody(AsyncWebServerRequest* request __attribute__((unused)), uint8_t* data __attribute__((unused)), size_t len __attribute__((unused)), size_t index __attribute__((unused)), size_t total __attribute__((unused))) {} + virtual void handleRequest(__unused AsyncWebServerRequest* request) {} + virtual void handleUpload(__unused AsyncWebServerRequest* request, __unused const String& filename, __unused size_t index, __unused uint8_t* data, __unused size_t len, __unused bool final) {} + virtual void handleBody(__unused AsyncWebServerRequest* request, __unused uint8_t* data, __unused size_t len, __unused size_t index, __unused size_t total) {} virtual bool isRequestHandlerTrivial() { return true; } }; @@ -588,6 +774,7 @@ class AsyncWebServerResponse { AsyncWebServerResponse(); virtual ~AsyncWebServerResponse(); virtual void setCode(int code); + int code() const { return _code; } virtual void setContentLength(size_t len); void setContentType(const String& type) { setContentType(type.c_str()); } virtual void setContentType(const char* type); @@ -597,6 +784,7 @@ class AsyncWebServerResponse { bool addHeader(const String& name, long value, bool replaceExisting = true) { return addHeader(name.c_str(), value, replaceExisting); } virtual bool removeHeader(const char* name); virtual const AsyncWebHeader* getHeader(const char* name) const; + const std::list& getHeaders() const { return _headers; } #ifndef ESP8266 [[deprecated("Use instead: _assembleHead(String& buffer, uint8_t version)")]] @@ -624,7 +812,7 @@ typedef std::function ArRequestHandlerFunc typedef std::function ArUploadHandlerFunction; typedef std::function ArBodyHandlerFunction; -class AsyncWebServer { +class AsyncWebServer : public AsyncMiddlewareChain { protected: AsyncServer _server; std::list> _rewrites; @@ -647,7 +835,7 @@ class AsyncWebServer { /** * @brief (compat) Add url rewrite rule by pointer - * a deep copy of the pounter object will be created, + * a deep copy of the pointer object will be created, * it is up to user to manage further lifetime of the object in argument * * @param rewrite pointer to rewrite object to copy setting from @@ -687,10 +875,8 @@ class AsyncWebServer { AsyncWebHandler& addHandler(AsyncWebHandler* handler); bool removeHandler(AsyncWebHandler* handler); - AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload); - AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody); + AsyncCallbackWebHandler& on(const char* uri, ArRequestHandlerFunction onRequest) { return on(uri, HTTP_ANY, onRequest); } + AsyncCallbackWebHandler& on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr, ArBodyHandlerFunction onBody = nullptr); AsyncStaticWebHandler& serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); diff --git a/src/Middleware.cpp b/src/Middleware.cpp new file mode 100644 index 0000000..1c36ef6 --- /dev/null +++ b/src/Middleware.cpp @@ -0,0 +1,253 @@ +#include "WebAuthentication.h" +#include + +AsyncMiddlewareChain::~AsyncMiddlewareChain() { + for (AsyncMiddleware* m : _middlewares) + if (m->_freeOnRemoval) + delete m; +} + +void AsyncMiddlewareChain::addMiddleware(ArMiddlewareCallback fn) { + AsyncMiddlewareFunction* m = new AsyncMiddlewareFunction(fn); + m->_freeOnRemoval = true; + _middlewares.emplace_back(m); +} + +void AsyncMiddlewareChain::addMiddleware(AsyncMiddleware* middleware) { + if (middleware) + _middlewares.emplace_back(middleware); +} + +void AsyncMiddlewareChain::addMiddlewares(std::vector middlewares) { + for (AsyncMiddleware* m : middlewares) + addMiddleware(m); +} + +bool AsyncMiddlewareChain::removeMiddleware(AsyncMiddleware* middleware) { + // remove all middlewares from _middlewares vector being equal to middleware, delete them having _freeOnRemoval flag to true and resize the vector. + const size_t size = _middlewares.size(); + _middlewares.erase(std::remove_if(_middlewares.begin(), _middlewares.end(), [middleware](AsyncMiddleware* m) { + if (m == middleware) { + if (m->_freeOnRemoval) + delete m; + return true; + } + return false; + }), + _middlewares.end()); + return size != _middlewares.size(); +} + +void AsyncMiddlewareChain::_runChain(AsyncWebServerRequest* request, ArMiddlewareNext finalizer) { + if (!_middlewares.size()) + return finalizer(); + ArMiddlewareNext next; + std::list::iterator it = _middlewares.begin(); + next = [this, &next, &it, request, finalizer]() { + if (it == _middlewares.end()) + return finalizer(); + AsyncMiddleware* m = *it; + it++; + return m->run(request, next); + }; + return next(); +} + +void AuthenticationMiddleware::setUsername(const char* username) { + _username = username; + _hasCreds = _username.length() && _credentials.length(); +} + +void AuthenticationMiddleware::setPassword(const char* password) { + _credentials = password; + _hash = false; + _hasCreds = _username.length() && _credentials.length(); +} + +void AuthenticationMiddleware::setPasswordHash(const char* hash) { + _credentials = hash; + _hash = true; + _hasCreds = _username.length() && _credentials.length(); +} + +bool AuthenticationMiddleware::generateHash() { + // ensure we have all the necessary data + if (!_hasCreds) + return false; + + // if we already have a hash, do nothing + if (_hash) + return false; + + switch (_authMethod) { + case AsyncAuthType::AUTH_DIGEST: + _credentials = generateDigestHash(_username.c_str(), _credentials.c_str(), _realm.c_str()); + _hash = true; + return true; + + case AsyncAuthType::AUTH_BASIC: + _credentials = generateBasicHash(_username.c_str(), _credentials.c_str()); + _hash = true; + return true; + + default: + return false; + } +} + +bool AuthenticationMiddleware::allowed(AsyncWebServerRequest* request) { + if (_authMethod == AsyncAuthType::AUTH_NONE) + return true; + + if (!_hasCreds) + return false; + + return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash); +} + +void AuthenticationMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + return allowed(request) ? next() : request->requestAuthentication(_authMethod, _realm.c_str(), _authFailMsg.c_str()); +} + +void HeaderFreeMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + std::vector reqHeaders; + request->getHeaderNames(reqHeaders); + for (const char* h : reqHeaders) { + bool keep = false; + for (const char* k : _toKeep) { + if (strcasecmp(h, k) == 0) { + keep = true; + break; + } + } + if (!keep) { + request->removeHeader(h); + } + } + next(); +} + +void HeaderFilterMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + for (auto it = _toRemove.begin(); it != _toRemove.end(); ++it) + request->removeHeader(*it); + next(); +} + +void LoggingMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + if (!isEnabled()) { + next(); + return; + } + _out->print(F("* Connection from ")); + _out->print(request->client()->remoteIP().toString()); + _out->print(':'); + _out->println(request->client()->remotePort()); + _out->print('>'); + _out->print(' '); + _out->print(request->methodToString()); + _out->print(' '); + _out->print(request->url().c_str()); + _out->print(F(" HTTP/1.")); + _out->println(request->version()); + for (auto& h : request->getHeaders()) { + if (h.value().length()) { + _out->print('>'); + _out->print(' '); + _out->print(h.name()); + _out->print(':'); + _out->print(' '); + _out->println(h.value()); + } + } + _out->println(F(">")); + uint32_t elapsed = millis(); + next(); + elapsed = millis() - elapsed; + AsyncWebServerResponse* response = request->getResponse(); + if (response) { + _out->print(F("* Processed in ")); + _out->print(elapsed); + _out->println(F(" ms")); + _out->print('<'); + _out->print(F(" HTTP/1.")); + _out->print(request->version()); + _out->print(' '); + _out->print(response->code()); + _out->print(' '); + _out->println(AsyncWebServerResponse::responseCodeToString(response->code())); + for (auto& h : response->getHeaders()) { + if (h.value().length()) { + _out->print('<'); + _out->print(' '); + _out->print(h.name()); + _out->print(':'); + _out->print(' '); + _out->println(h.value()); + } + } + _out->println('<'); + } else { + _out->println(F("* Connection closed!")); + } +} + +void CorsMiddleware::addCORSHeaders(AsyncWebServerResponse* response) { + response->addHeader(F("Access-Control-Allow-Origin"), _origin.c_str()); + response->addHeader(F("Access-Control-Allow-Methods"), _methods.c_str()); + response->addHeader(F("Access-Control-Allow-Headers"), _headers.c_str()); + response->addHeader(F("Access-Control-Allow-Credentials"), _credentials ? F("true") : F("false")); + response->addHeader(F("Access-Control-Max-Age"), String(_maxAge).c_str()); +} + +void CorsMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + // Origin header ? => CORS handling + if (request->hasHeader(F("Origin"))) { + // check if this is a preflight request => handle it and return + if (request->method() == HTTP_OPTIONS) { + AsyncWebServerResponse* response = request->beginResponse(200); + addCORSHeaders(response); + request->send(response); + return; + } + + // CORS request, no options => let the request pass and add CORS headers after + next(); + AsyncWebServerResponse* response = request->getResponse(); + if (response) { + addCORSHeaders(response); + } + + } else { + // NO Origin header => no CORS handling + next(); + } +} + +bool RateLimitMiddleware::isRequestAllowed(uint32_t& retryAfterSeconds) { + uint32_t now = millis(); + + while (!_requestTimes.empty() && _requestTimes.front() <= now - _windowSizeMillis) + _requestTimes.pop_front(); + + _requestTimes.push_back(now); + + if (_requestTimes.size() > _maxRequests) { + _requestTimes.pop_front(); + retryAfterSeconds = (_windowSizeMillis - (now - _requestTimes.front())) / 1000 + 1; + return false; + } + + retryAfterSeconds = 0; + return true; +} + +void RateLimitMiddleware::run(AsyncWebServerRequest* request, ArMiddlewareNext next) { + uint32_t retryAfterSeconds; + if (isRequestAllowed(retryAfterSeconds)) { + next(); + } else { + AsyncWebServerResponse* response = request->beginResponse(429); + response->addHeader(F("Retry-After"), retryAfterSeconds); + request->send(response); + } +} diff --git a/src/WebAuthentication.cpp b/src/WebAuthentication.cpp index b5962fc..304f258 100644 --- a/src/WebAuthentication.cpp +++ b/src/WebAuthentication.cpp @@ -34,36 +34,34 @@ using namespace asyncsrv; bool checkBasicAuthentication(const char* hash, const char* username, const char* password) { if (username == NULL || password == NULL || hash == NULL) return false; + return generateBasicHash(username, password).equalsIgnoreCase(hash); +} + +String generateBasicHash(const char* username, const char* password) { + if (username == NULL || password == NULL) + return emptyString; size_t toencodeLen = strlen(username) + strlen(password) + 1; - size_t encodedLen = base64_encode_expected_len(toencodeLen); - if (strlen(hash) != encodedLen) -// Fix from https://github.com/me-no-dev/ESPAsyncWebServer/issues/667 -#ifdef ARDUINO_ARCH_ESP32 - if (strlen(hash) != encodedLen) -#else - if (strlen(hash) != encodedLen - 1) -#endif - return false; char* toencode = new char[toencodeLen + 1]; if (toencode == NULL) { - return false; + return emptyString; } char* encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; if (encoded == NULL) { delete[] toencode; - return false; + return emptyString; } sprintf_P(toencode, PSTR("%s:%s"), username, password); - if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0) { + if (base64_encode_chars(toencode, toencodeLen, encoded) > 0) { + String res = String(encoded); delete[] toencode; delete[] encoded; - return true; + return res; } delete[] toencode; delete[] encoded; - return false; + return emptyString; } static bool getMD5(uint8_t* data, uint16_t len, char* output) { // 33 bytes or more @@ -94,7 +92,7 @@ static bool getMD5(uint8_t* data, uint16_t len, char* output) { // 33 bytes or m return true; } -static String genRandomMD5() { +String genRandomMD5() { #ifdef ESP8266 uint32_t r = RANDOM_REG32; #else @@ -122,31 +120,21 @@ String generateDigestHash(const char* username, const char* password, const char return emptyString; } char* out = (char*)malloc(33); - String res = String(username); - res += ':'; - res.concat(realm); - res += ':'; - String in = res; + + String in; + in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2); + in.concat(username); + in.concat(':'); + in.concat(realm); + in.concat(':'); in.concat(password); + if (out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) return emptyString; - res.concat(out); - free(out); - return res; -} -String requestDigestAuthentication(const char* realm) { - String header(T_realm__); - if (realm == NULL) - header.concat(T_asyncesp); - else - header.concat(realm); - header.concat(T_auth_nonce); - header.concat(genRandomMD5()); - header.concat(T__opaque); - header.concat(genRandomMD5()); - header += (char)0x22; // '"' - return header; + in = String(out); + free(out); + return in; } #ifndef ESP8266 @@ -235,9 +223,9 @@ bool checkDigestAuthentication(const char* header, const __FlashStringHelper* me } } while (nextBreak > 0); - String ha1 = (passwordIsHash) ? String(password) : stringMD5(myUsername + ':' + myRealm + ':' + password); - String ha2 = String(method) + ':' + myUri; - String response = ha1 + ':' + myNonce + ':' + myNc + ':' + myCnonce + ':' + myQop + ':' + stringMD5(ha2); + String ha1 = passwordIsHash ? password : stringMD5(myUsername + ':' + myRealm + ':' + password).c_str(); + String ha2 = stringMD5(String(method) + ':' + myUri); + String response = ha1 + ':' + myNonce + ':' + myNc + ':' + myCnonce + ':' + myQop + ':' + ha2; if (myResponse.equals(stringMD5(response))) { // os_printf("AUTH SUCCESS\n"); diff --git a/src/WebAuthentication.h b/src/WebAuthentication.h index d519777..a35d551 100644 --- a/src/WebAuthentication.h +++ b/src/WebAuthentication.h @@ -25,7 +25,6 @@ #include "Arduino.h" bool checkBasicAuthentication(const char* header, const char* username, const char* password); -String requestDigestAuthentication(const char* realm); bool checkDigestAuthentication(const char* header, const char* method, const char* username, const char* password, const char* realm, bool passwordIsHash, const char* nonce, const char* opaque, const char* uri); @@ -36,4 +35,8 @@ bool checkDigestAuthentication(const char* header, const __FlashStringHelper* me // for storing hashed versions on the device that can be authenticated against String generateDigestHash(const char* username, const char* password, const char* realm); +String generateBasicHash(const char* username, const char* password); + +String genRandomMD5(); + #endif diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index 22757d7..cb43f80 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -63,10 +63,7 @@ class AsyncStaticWebHandler : public AsyncWebHandler { AsyncStaticWebHandler& setLastModified(time_t last_modified); AsyncStaticWebHandler& setLastModified(); // sets to current time. Make sure sntp is runing and time is updated #endif - AsyncStaticWebHandler& setTemplateProcessor(AwsTemplateProcessor newCallback) { - _callback = newCallback; - return *this; - } + AsyncStaticWebHandler& setTemplateProcessor(AwsTemplateProcessor newCallback); }; class AsyncCallbackWebHandler : public AsyncWebHandler { @@ -81,75 +78,17 @@ class AsyncCallbackWebHandler : public AsyncWebHandler { public: AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} - void setUri(const String& uri) { - _uri = uri; - _isRegex = uri.startsWith("^") && uri.endsWith("$"); - } + void setUri(const String& uri); void setMethod(WebRequestMethodComposite method) { _method = method; } void onRequest(ArRequestHandlerFunction fn) { _onRequest = fn; } void onUpload(ArUploadHandlerFunction fn) { _onUpload = fn; } void onBody(ArBodyHandlerFunction fn) { _onBody = fn; } - virtual bool canHandle(AsyncWebServerRequest* request) override final { - - if (!_onRequest) - return false; - - if (!(_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; - - request->addInterestingHeader("ANY"); - return true; - } - - virtual void handleRequest(AsyncWebServerRequest* request) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onRequest) - _onRequest(request); - else - request->send(500); - } - virtual void handleUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onUpload) - _onUpload(request, filename, index, data, len, final); - } - virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final { - if ((_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); - if (_onBody) - _onBody(request, data, len, index, total); - } - virtual bool isRequestHandlerTrivial() override final { return _onRequest ? false : true; } + virtual bool canHandle(AsyncWebServerRequest* request) override final; + virtual void handleRequest(AsyncWebServerRequest* request) override final; + virtual void handleUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) override final; + virtual void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) override final; + virtual bool isRequestHandlerTrivial() override final { return !_onRequest; } }; #endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */ diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index d904a39..7c3a7dc 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -23,6 +23,20 @@ using namespace asyncsrv; +AsyncWebHandler& AsyncWebHandler::setFilter(ArRequestFilterFunction fn) { + _filter = fn; + return *this; +} +AsyncWebHandler& AsyncWebHandler::setAuthentication(const char* username, const char* password) { + if (!_authMiddleware) { + _authMiddleware = new AuthenticationMiddleware(); + _authMiddleware->_freeOnRemoval = true; + addMiddleware(_authMiddleware); + } + _authMiddleware->setUsername(username); + _authMiddleware->setPassword(password); + return *this; +}; AsyncStaticWebHandler::AsyncStaticWebHandler(const char* uri, FS& fs, const char* path, const char* cache_control) : _fs(fs), _uri(uri), _path(path), _default_file(F("index.htm")), _cache_control(cache_control), _last_modified(), _callback(nullptr) { @@ -94,18 +108,7 @@ bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest* request) { if (request->method() != HTTP_GET || !request->url().startsWith(_uri) || !request->isExpectedRequestedConnType(RCT_DEFAULT, RCT_HTTP)) { return false; } - if (_getFile(request)) { - // We interested in "If-Modified-Since" header to check if file was modified - if (_last_modified.length()) - request->addInterestingHeader(F("If-Modified-Since")); - - if (_cache_control.length()) - request->addInterestingHeader(F("If-None-Match")); - - return true; - } - - return false; + return _getFile(request); } bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest* request) { @@ -204,8 +207,6 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest* request) { String filename = String((char*)request->_tempObject); free(request->_tempObject); request->_tempObject = NULL; - if ((_username.length() && _password.length()) && !request->authenticate(_username.c_str(), _password.c_str())) - return request->requestAuthentication(); if (request->_tempFile == true) { time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS) @@ -248,3 +249,65 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest* request) { request->send(404); } } + +AsyncStaticWebHandler& AsyncStaticWebHandler::setTemplateProcessor(AwsTemplateProcessor newCallback) { + _callback = newCallback; + return *this; +} + +void AsyncCallbackWebHandler::setUri(const String& uri) { + _uri = uri; + _isRegex = uri.startsWith("^") && uri.endsWith("$"); +} + +bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest* request) { + if (!_onRequest) + return false; + + if (!(_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; +} + +void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest* request) { + if (_onRequest) + _onRequest(request); + else + request->send(500); +} +void AsyncCallbackWebHandler::handleUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) { + if (_onUpload) + _onUpload(request, filename, index, data, len, final); +} +void AsyncCallbackWebHandler::handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) { + if (_onBody) + _onBody(request, data, len, index, total); +} \ No newline at end of file diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 022911e..242a111 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -35,7 +35,7 @@ enum { PARSE_REQ_START, PARSE_REQ_FAIL }; AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) - : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(0), _version(0), _method(HTTP_ANY), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _isDigest(false), _isMultipart(false), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { + : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(0), _version(0), _method(HTTP_ANY), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { c->onError([](void* r, AsyncClient* c, int8_t error) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); c->onAck([](void* r, AsyncClient* c, size_t len, uint32_t time) { (void)c; AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onAck(len, time); }, this); c->onDisconnect([](void* r, AsyncClient* c) { AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onDisconnect(); delete c; }, this); @@ -49,8 +49,6 @@ AsyncWebServerRequest::~AsyncWebServerRequest() { _pathParams.clear(); - _interestingHeaders.clear(); - if (_response != NULL) { delete _response; } @@ -125,7 +123,6 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) { } } if (!_isPlainPost) { - // check if authenticated before calling the body if (_handler) _handler->handleBody(this, (uint8_t*)buf, len, _parsedLength, _contentLength); _parsedLength += len; @@ -141,31 +138,20 @@ void AsyncWebServerRequest::_onData(void* buf, size_t len) { } if (_parsedLength == _contentLength) { _parseState = PARSE_REQ_END; - // check if authenticated before calling handleRequest and request auth instead - if (_handler) - _handler->handleRequest(this); - else - send(501); + _server->_runChain(this, [this]() { return _handler ? _handler->_runChain(this, [this]() { _handler->handleRequest(this); }) : send(501); }); + if (!_sent) { + if (!_response) + send(501, T_text_plain, "Handler did not handle the request"); + _client->setRxTimeout(0); + _response->_respond(this); + _sent = true; + } } } break; } } -void AsyncWebServerRequest::_removeNotInterestingHeaders() { - if (std::any_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [](const String& str) { return str.equalsIgnoreCase(T_ANY); })) - return; // nothing to do - - for (auto iter = std::begin(_headers); iter != std::end(_headers);) { - const auto name = iter->name(); - - if (std::none_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [&name](const String& str) { return str.equalsIgnoreCase(name); })) - iter = _headers.erase(iter); - else - iter++; - } -} - void AsyncWebServerRequest::_onPoll() { // os_printf("p\n"); if (_response != NULL && _client != NULL && _client->canSend()) { @@ -299,9 +285,16 @@ bool AsyncWebServerRequest::_parseReqHeader() { } else if (name.equalsIgnoreCase(T_AUTH)) { if (value.length() > 5 && value.substring(0, 5).equalsIgnoreCase(T_BASIC)) { _authorization = value.substring(6); + _authMethod = AsyncAuthType::AUTH_BASIC; } else if (value.length() > 6 && value.substring(0, 6).equalsIgnoreCase(T_DIGEST)) { - _isDigest = true; + _authMethod = AsyncAuthType::AUTH_DIGEST; _authorization = value.substring(7); + } else if (value.length() > 6 && value.substring(0, 6).equalsIgnoreCase(T_BEARER)) { + _authMethod = AsyncAuthType::AUTH_BEARER; + _authorization = value.substring(7); + } else { + _authorization = value; + _authMethod = AsyncAuthType::AUTH_OTHER; } } else { if (name.equalsIgnoreCase(T_UPGRADE) && value.equalsIgnoreCase(T_WS)) { @@ -356,7 +349,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last) { _itemBuffer[_itemBufferIndex++] = data; - if (last || _itemBufferIndex == 1460) { + if (last || _itemBufferIndex == RESPONSE_STREAM_BUFFER_SIZE) { // check if authenticated before calling the upload if (_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); @@ -460,7 +453,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { if (_itemIsFile) { if (_itemBuffer) free(_itemBuffer); - _itemBuffer = (uint8_t*)malloc(1460); + _itemBuffer = (uint8_t*)malloc(RESPONSE_STREAM_BUFFER_SIZE); if (_itemBuffer == NULL) { _multiParseState = PARSE_ERROR; return; @@ -514,7 +507,6 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { _params.emplace_back(_itemName, _itemValue, true); } else { if (_itemSize) { - // check if authenticated before calling the upload if (_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); _itemBufferIndex = 0; @@ -583,20 +575,22 @@ void AsyncWebServerRequest::_parseLine() { // end of headers _server->_rewriteRequest(this); _server->_attachHandler(this); - _removeNotInterestingHeaders(); if (_expectingContinue) { String response(T_HTTP_100_CONT); _client->write(response.c_str(), response.length()); } - // check handler for authentication if (_contentLength) { _parseState = PARSE_REQ_BODY; } else { _parseState = PARSE_REQ_END; - if (_handler) - _handler->handleRequest(this); - else - send(501); + _server->_runChain(this, [this]() { return _handler ? _handler->_runChain(this, [this]() { _handler->handleRequest(this); }) : send(501); }); + if (!_sent) { + if (!_response) + send(501, T_text_plain, "Handler did not handle the request"); + _client->setRxTimeout(0); + _response->_respond(this); + _sent = true; + } } } else _parseReqHeader(); @@ -649,6 +643,21 @@ const AsyncWebHeader* AsyncWebServerRequest::getHeader(size_t num) const { return &(*std::next(_headers.cbegin(), num)); } +size_t AsyncWebServerRequest::getHeaderNames(std::vector& names) const { + const size_t size = _headers.size(); + names.reserve(size); + for (const auto& h : _headers) { + names.push_back(h.name().c_str()); + } + return size; +} + +bool AsyncWebServerRequest::removeHeader(const char* name) { + const size_t size = _headers.size(); + _headers.remove_if([name](const AsyncWebHeader& header) { return header.name().equalsIgnoreCase(name); }); + return size != _headers.size(); +} + size_t AsyncWebServerRequest::params() const { return _params.size(); } @@ -683,9 +692,25 @@ const AsyncWebParameter* AsyncWebServerRequest::getParam(size_t num) const { return &(*std::next(_params.cbegin(), num)); } -void AsyncWebServerRequest::addInterestingHeader(const char* name) { - if (std::none_of(std::begin(_interestingHeaders), std::end(_interestingHeaders), [&name](const String& str) { return str.equalsIgnoreCase(name); })) - _interestingHeaders.emplace_back(name); +const String& AsyncWebServerRequest::getAttribute(const char* name, const String& defaultValue) const { + auto it = _attributes.find(name); + return it != _attributes.end() ? it->second : defaultValue; +} +bool AsyncWebServerRequest::getAttribute(const char* name, bool defaultValue) const { + auto it = _attributes.find(name); + return it != _attributes.end() ? it->second == "1" : defaultValue; +} +long AsyncWebServerRequest::getAttribute(const char* name, long defaultValue) const { + auto it = _attributes.find(name); + return it != _attributes.end() ? it->second.toInt() : defaultValue; +} +float AsyncWebServerRequest::getAttribute(const char* name, float defaultValue) const { + auto it = _attributes.find(name); + return it != _attributes.end() ? it->second.toFloat() : defaultValue; +} +double AsyncWebServerRequest::getAttribute(const char* name, double defaultValue) const { + auto it = _attributes.find(name); + return it != _attributes.end() ? it->second.toDouble() : defaultValue; } AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(int code, const char* contentType, const char* content, AwsTemplateProcessor callback) { @@ -728,38 +753,35 @@ AsyncResponseStream* AsyncWebServerRequest::beginResponseStream(const char* cont return new AsyncResponseStream(contentType, bufferSize); } -#ifdef ESP8266 -AsyncWebServerResponse* AsyncWebServerRequest::beginResponse(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback) { +AsyncWebServerResponse* AsyncWebServerRequest::beginResponse_P(int code, const String& contentType, PGM_P content, AwsTemplateProcessor callback) { return new AsyncProgmemResponse(code, contentType, (const uint8_t*)content, strlen_P(content), callback); } -#endif void AsyncWebServerRequest::send(AsyncWebServerResponse* response) { + if (_sent) + return; + if (_response) + delete _response; _response = response; if (_response == NULL) { _client->close(true); _onDisconnect(); + _sent = true; return; } - if (!_response->_sourceValid()) { - delete response; - _response = NULL; + if (!_response->_sourceValid()) send(500); - } else { - _client->setRxTimeout(0); - _response->_respond(this); - } } -void AsyncWebServerRequest::redirect(const char* url) { - AsyncWebServerResponse* response = beginResponse(302); +void AsyncWebServerRequest::redirect(const char* url, int code) { + AsyncWebServerResponse* response = beginResponse(code); response->addHeader(T_LOCATION, url); send(response); } bool AsyncWebServerRequest::authenticate(const char* username, const char* password, const char* realm, bool passwordIsHash) { if (_authorization.length()) { - if (_isDigest) + if (_authMethod == AsyncAuthType::AUTH_DIGEST) return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); else if (!passwordIsHash) return checkBasicAuthentication(_authorization.c_str(), username, password); @@ -773,7 +795,7 @@ bool AsyncWebServerRequest::authenticate(const char* hash) { if (!_authorization.length() || hash == NULL) return false; - if (_isDigest) { + if (_authMethod == AsyncAuthType::AUTH_DIGEST) { String hStr = String(hash); int separator = hStr.indexOf(':'); if (separator <= 0) @@ -788,23 +810,45 @@ bool AsyncWebServerRequest::authenticate(const char* hash) { return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); } + // Basic Auth, Bearer Auth, or other return (_authorization.equals(hash)); } -void AsyncWebServerRequest::requestAuthentication(const char* realm, bool isDigest) { - AsyncWebServerResponse* r = beginResponse(401); - if (!isDigest && realm == NULL) { - r->addHeader(T_WWW_AUTH, T_BASIC_REALM_LOGIN_REQ); - } else if (!isDigest) { - String header(T_BASIC_REALM); - header.concat(realm); - header += '"'; - r->addHeader(T_WWW_AUTH, header.c_str()); - } else { - String header(T_DIGEST_); - header.concat(requestDigestAuthentication(realm)); - r->addHeader(T_WWW_AUTH, header.c_str()); +void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const char* realm, const char* _authFailMsg) { + if (!realm) + realm = T_LOGIN_REQ; + + AsyncWebServerResponse* r = _authFailMsg ? beginResponse(401, T_text_html, _authFailMsg) : beginResponse(401); + + switch (method) { + case AsyncAuthType::AUTH_BASIC: { + String header; + header.reserve(strlen(T_BASIC_REALM) + strlen(realm) + 1); + header.concat(T_BASIC_REALM); + header.concat(realm); + header.concat('"'); + r->addHeader(T_WWW_AUTH, header.c_str()); + break; + } + case AsyncAuthType::AUTH_DIGEST: { + constexpr size_t len = strlen(T_DIGEST_) + strlen(T_realm__) + strlen(T_auth_nonce) + 32 + strlen(T__opaque) + 32 + 1; + String header; + header.reserve(len + strlen(realm)); + header.concat(T_DIGEST_); + header.concat(T_realm__); + header.concat(realm); + header.concat(T_auth_nonce); + header.concat(genRandomMD5()); + header.concat(T__opaque); + header.concat(genRandomMD5()); + header.concat((char)0x22); // '"' + r->addHeader(T_WWW_AUTH, header.c_str()); + break; + } + default: + break; } + send(r); } diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 1fb6188..e7edf5e 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -109,6 +109,8 @@ const char* AsyncWebServerResponse::responseCodeToString(int code) { 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: @@ -196,6 +198,8 @@ const __FlashStringHelper* AsyncWebServerResponse::responseCodeToString(int code return FPSTR(T_HTTP_CODE_416); case 417: return FPSTR(T_HTTP_CODE_417); + case 429: + return FPSTR(T_HTTP_CODE_429); case 500: return FPSTR(T_HTTP_CODE_500); case 501: @@ -318,7 +322,6 @@ void AsyncWebServerResponse::_assembleHead(String& buffer, uint8_t version) { buffer.concat(header.value()); buffer.concat(T_rn); } - _headers.clear(); buffer.concat(T_rn); _headLength = buffer.length(); @@ -492,7 +495,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u free(buf); return 0; } - outLen = sprintf((char*)buf+headLen, "%04x", readLen) + headLen; + outLen = sprintf((char*)buf + headLen, "%04x", readLen) + headLen; buf[outLen++] = '\r'; buf[outLen++] = '\n'; outLen += readLen; diff --git a/src/WebServer.cpp b/src/WebServer.cpp index d7c9a02..588bd01 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -24,19 +24,19 @@ using namespace asyncsrv; bool ON_STA_FILTER(AsyncWebServerRequest* request) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.localIP() == request->client()->localIP(); - #else +#else return false; - #endif +#endif } bool ON_AP_FILTER(AsyncWebServerRequest* request) { - #ifndef CONFIG_IDF_TARGET_ESP32H2 +#ifndef CONFIG_IDF_TARGET_ESP32H2 return WiFi.localIP() != request->client()->localIP(); - #else +#else return false; - #endif +#endif } #ifndef HAVE_FS_FILE_OPEN_MODE @@ -155,7 +155,6 @@ void AsyncWebServer::_attachHandler(AsyncWebServerRequest* request) { } } - request->addInterestingHeader(T_ANY); request->setHandler(_catchAllHandler); } @@ -170,33 +169,6 @@ AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodCom return *handler; } -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->setMethod(method); - handler->onRequest(onRequest); - handler->onUpload(onUpload); - addHandler(handler); - return *handler; -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->setMethod(method); - handler->onRequest(onRequest); - addHandler(handler); - return *handler; -} - -AsyncCallbackWebHandler& AsyncWebServer::on(const char* uri, ArRequestHandlerFunction onRequest) { - AsyncCallbackWebHandler* handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); - handler->onRequest(onRequest); - addHandler(handler); - return *handler; -} - 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); diff --git a/src/literals.h b/src/literals.h index 40b596a..2300b4a 100644 --- a/src/literals.h +++ b/src/literals.h @@ -12,7 +12,7 @@ static constexpr const char* T_app_xform_urlencoded = "application/x-www-form-ur static constexpr const char* T_AUTH = "Authorization"; static constexpr const char* T_BASIC = "Basic"; static constexpr const char* T_BASIC_REALM = "Basic realm=\""; -static constexpr const char* T_BASIC_REALM_LOGIN_REQ = "Basic realm=\"Login Required\""; +static constexpr const char* T_LOGIN_REQ = "Login Required"; static constexpr const char* T_BODY = "body"; static constexpr const char* T_Cache_Control = "Cache-Control"; static constexpr const char* T_chunked = "chunked"; @@ -25,6 +25,7 @@ static constexpr const char* T_Content_Type = "Content-Type"; static constexpr const char* T_Cookie = "Cookie"; static constexpr const char* T_DIGEST = "Digest"; static constexpr const char* T_DIGEST_ = "Digest "; +static constexpr const char* T_BEARER = "Bearer"; static constexpr const char* T_ETag = "ETag"; static constexpr const char* T_EXPECT = "Expect"; static constexpr const char* T_HTTP_1_0 = "HTTP/1.0"; @@ -137,6 +138,7 @@ 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"; @@ -148,7 +150,6 @@ static constexpr const char* T_HTTP_CODE_ANY = "Unknown code"; // other static constexpr const char* T__opaque = "\", opaque=\""; static constexpr const char* T_13 = "13"; -static constexpr const char* T_asyncesp = "asyncesp"; static constexpr const char* T_auth_nonce = "\", qop=\"auth\", nonce=\""; static constexpr const char* T_cnonce = "cnonce"; static constexpr const char* T_data_ = "data: "; @@ -181,7 +182,7 @@ static const char T_app_xform_urlencoded[] PROGMEM = "application/x-www-form-url static const char T_AUTH[] PROGMEM = "Authorization"; static const char T_BASIC[] PROGMEM = "Basic"; static const char T_BASIC_REALM[] PROGMEM = "Basic realm=\""; -static const char T_BASIC_REALM_LOGIN_REQ[] PROGMEM = "Basic realm=\"Login Required\""; +static const char T_LOGIN_REQ[] PROGMEM = "Login Required"; static const char T_BODY[] PROGMEM = "body"; static const char T_Cache_Control[] PROGMEM = "Cache-Control"; static const char T_chunked[] PROGMEM = "chunked"; @@ -194,6 +195,7 @@ static const char T_Content_Type[] PROGMEM = "Content-Type"; static const char T_Cookie[] PROGMEM = "Cookie"; static const char T_DIGEST[] PROGMEM = "Digest"; static const char T_DIGEST_[] PROGMEM = "Digest "; +static const char T_BEARER[] PROGMEM = "Bearer"; static const char T_ETag[] PROGMEM = "ETag"; static const char T_EXPECT[] PROGMEM = "Expect"; static const char T_HTTP_1_0[] PROGMEM = "HTTP/1.0"; @@ -306,6 +308,7 @@ static const char T_HTTP_CODE_414[] PROGMEM = "Request-URI Too Large"; static const char T_HTTP_CODE_415[] PROGMEM = "Unsupported Media Type"; static const char T_HTTP_CODE_416[] PROGMEM = "Requested range not satisfiable"; static const char T_HTTP_CODE_417[] PROGMEM = "Expectation Failed"; +static const char T_HTTP_CODE_429[] PROGMEM = "Too Many Requests"; static const char T_HTTP_CODE_500[] PROGMEM = "Internal Server Error"; static const char T_HTTP_CODE_501[] PROGMEM = "Not Implemented"; static const char T_HTTP_CODE_502[] PROGMEM = "Bad Gateway"; @@ -317,7 +320,6 @@ static const char T_HTTP_CODE_ANY[] PROGMEM = "Unknown code"; // other static const char T__opaque[] PROGMEM = "\", opaque=\""; static const char T_13[] PROGMEM = "13"; -static const char T_asyncesp[] PROGMEM = "asyncesp"; static const char T_auth_nonce[] PROGMEM = "\", qop=\"auth\", nonce=\""; static const char T_cnonce[] PROGMEM = "cnonce"; static const char T_data_[] PROGMEM = "data: ";