diff options
| author | schererleander <leander@schererleander.de> | 2026-01-20 08:34:54 +0100 |
|---|---|---|
| committer | schererleander <leander@schererleander.de> | 2026-01-20 08:34:54 +0100 |
| commit | 85ea4e995a75abe061f6fc375ea0481084dddd43 (patch) | |
| tree | 7eb5d57653ecd8f041aeac4e68d7d554c1168681 /libraries/ESP_Async_WebServer/src | |
Diffstat (limited to 'libraries/ESP_Async_WebServer/src')
25 files changed, 8459 insertions, 0 deletions
diff --git a/libraries/ESP_Async_WebServer/src/AsyncEventSource.cpp b/libraries/ESP_Async_WebServer/src/AsyncEventSource.cpp new file mode 100644 index 0000000..2ebfa2d --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncEventSource.cpp @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "Arduino.h" +#if defined(ESP32) +#include <rom/ets_sys.h> +#endif +#include "AsyncEventSource.h" + +#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa + +using namespace asyncsrv; + +static String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + String str; + size_t len{0}; + if (message) { + len += strlen(message); + } + + if (event) { + len += strlen(event); + } + + len += 42; // give it some overhead + + if (!str.reserve(len)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + return emptyString; + } + + if (reconnect) { + str += T_retry_; + str += reconnect; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' + } + + if (id) { + str += T_id__; + str += id; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' + } + + if (event != NULL) { + str += T_event_; + str += event; + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' + } + + if (!message) { + return str; + } + + size_t messageLen = strlen(message); + char *lineStart = (char *)message; + char *lineEnd; + do { + char *nextN = strchr(lineStart, '\n'); + char *nextR = strchr(lineStart, '\r'); + if (nextN == NULL && nextR == NULL) { + // a message is a single-line string + str += T_data_; + str += message; + str += T_nn; + return str; + } + + // a message is a multi-line string + char *nextLine = NULL; + if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n + if (nextR + 1 == nextN) { + // normal \r\n sequence + lineEnd = nextR; + nextLine = nextN + 1; + } else { + // some abnormal \n \r mixed sequence + lineEnd = std::min(nextR, nextN); + nextLine = lineEnd + 1; + } + } else if (nextN != NULL) { // Unix/Mac OS X LF + lineEnd = nextN; + nextLine = nextN + 1; + } else { // some ancient garbage + lineEnd = nextR; + nextLine = nextR + 1; + } + + str += T_data_; + str.concat(lineStart, lineEnd - lineStart); + str += ASYNC_SSE_NEW_LINE_CHAR; // \n + + lineStart = nextLine; + } while (lineStart < ((char *)message + messageLen)); + + // append another \n to terminate message + str += ASYNC_SSE_NEW_LINE_CHAR; // '\n' + + return str; +} + +// Message + +size_t AsyncEventSourceMessage::ack(size_t len, __attribute__((unused)) uint32_t time) { + // If the whole message is now acked... + if (_acked + len > _data->length()) { + // Return the number of extra bytes acked (they will be carried on to the next message) + const size_t extra = _acked + len - _data->length(); + _acked = _data->length(); + return extra; + } + // Return that no extra bytes left. + _acked += len; + return 0; +} + +size_t AsyncEventSourceMessage::write(AsyncClient *client) { + if (!client) { + return 0; + } + + if (_sent >= _data->length() || !client->canSend()) { + return 0; + } + + size_t len = std::min(_data->length() - _sent, client->space()); + /* + add() would call lwip's tcp_write() under the AsyncTCP hood with apiflags argument. + By default apiflags=ASYNC_WRITE_FLAG_COPY + we could have used apiflags with this flag unset to pass data by reference and avoid copy to socket buffer, + but looks like it does not work for Arduino's lwip in ESP32/IDF + it is enforced in https://github.com/espressif/esp-lwip/blob/0606eed9d8b98a797514fdf6eabb4daf1c8c8cd9/src/core/tcp_out.c#L422C5-L422C30 + if LWIP_NETIF_TX_SINGLE_PBUF is set, and it is set indeed in IDF + https://github.com/espressif/esp-idf/blob/a0f798cfc4bbd624aab52b2c194d219e242d80c1/components/lwip/port/include/lwipopts.h#L744 + + So let's just keep it enforced ASYNC_WRITE_FLAG_COPY and keep in mind that there is no zero-copy + */ + size_t written = client->add(_data->c_str() + _sent, len, ASYNC_WRITE_FLAG_COPY); // ASYNC_WRITE_FLAG_MORE + _sent += written; + return written; +} + +size_t AsyncEventSourceMessage::send(AsyncClient *client) { + size_t sent = write(client); + return sent && client->send() ? sent : 0; +} + +// Client + +AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->client()), _server(server) { + + if (request->hasHeader(T_Last_Event_ID)) { + _lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str()); + } + + _client->setRxTimeout(0); + _client->onError(NULL, NULL); + _client->onAck( + [](void *r, AsyncClient *c, size_t len, uint32_t time) { + (void)c; + static_cast<AsyncEventSourceClient *>(r)->_onAck(len, time); + }, + this + ); + _client->onPoll( + [](void *r, AsyncClient *c) { + (void)c; + static_cast<AsyncEventSourceClient *>(r)->_onPoll(); + }, + this + ); + _client->onData(NULL, NULL); + _client->onTimeout( + [this](void *r, AsyncClient *c __attribute__((unused)), uint32_t time) { + static_cast<AsyncEventSourceClient *>(r)->_onTimeout(time); + }, + this + ); + _client->onDisconnect( + [this](void *r, AsyncClient *c) { + static_cast<AsyncEventSourceClient *>(r)->_onDisconnect(); + delete c; + }, + this + ); + + _server->_addClient(this); + delete request; + + _client->setNoDelay(true); +} + +AsyncEventSourceClient::~AsyncEventSourceClient() { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lockmq); +#endif + _messageQueue.clear(); + close(); +} + +bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) { + if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) { +#ifdef ESP8266 + ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); +#elif defined(ESP32) + log_e("Event message queue overflow: discard message"); +#endif + return false; + } + +#ifdef ESP32 + // length() is not thread-safe, thus acquiring the lock before this call.. + std::lock_guard<std::recursive_mutex> lock(_lockmq); +#endif + + _messageQueue.emplace_back(message, len); + + /* + throttle queue run + if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff + forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue + the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP + */ + if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) { + _runQueue(); + } + + return true; +} + +bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { + if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) { +#ifdef ESP8266 + ets_printf(String(F("ERROR: Too many messages queued\n")).c_str()); +#elif defined(ESP32) + log_e("Event message queue overflow: discard message"); +#endif + return false; + } + +#ifdef ESP32 + // length() is not thread-safe, thus acquiring the lock before this call.. + std::lock_guard<std::recursive_mutex> lock(_lockmq); +#endif + + _messageQueue.emplace_back(std::move(msg)); + + /* + throttle queue run + if Q is filled for >25% then network/CPU is congested, since there is no zero-copy mode for socket buff + forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue + the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP + */ + if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) { + _runQueue(); + } + return true; +} + +void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { +#ifdef ESP32 + // Same here, acquiring the lock early + std::lock_guard<std::recursive_mutex> lock(_lockmq); +#endif + + // adjust in-flight len + if (len < _inflight) { + _inflight -= len; + } else { + _inflight = 0; + } + + // acknowledge as much messages's data as we got confirmed len from a AsyncTCP + while (len && _messageQueue.size()) { + len = _messageQueue.front().ack(len); + if (_messageQueue.front().finished()) { + // now we could release full ack'ed messages, we were keeping it unless send confirmed from AsyncTCP + _messageQueue.pop_front(); + } + } + + // try to send another batch of data + if (_messageQueue.size()) { + _runQueue(); + } +} + +void AsyncEventSourceClient::_onPoll() { + if (_messageQueue.size()) { +#ifdef ESP32 + // Same here, acquiring the lock early + std::lock_guard<std::recursive_mutex> lock(_lockmq); +#endif + _runQueue(); + } +} + +void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))) { + if (_client) { + _client->close(true); + } +} + +void AsyncEventSourceClient::_onDisconnect() { + if (!_client) { + return; + } + _client = nullptr; + _server->_handleDisconnect(this); +} + +void AsyncEventSourceClient::close() { + if (_client) { + _client->close(); + } +} + +bool AsyncEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + if (!connected()) { + return false; + } + return _queueMessage(std::make_shared<String>(generateEventMessage(message, event, id, reconnect))); +} + +void AsyncEventSourceClient::_runQueue() { + if (!_client) { + return; + } + + // there is no need to lock the mutex here, 'cause all the calls to this method must be already lock'ed + size_t total_bytes_written = 0; + for (auto i = _messageQueue.begin(); i != _messageQueue.end(); ++i) { + if (!i->sent()) { + const size_t bytes_written = i->write(_client); + total_bytes_written += bytes_written; + _inflight += bytes_written; + if (bytes_written == 0 || _inflight > _max_inflight) { + // Serial.print("_"); + break; + } + } + } + + // flush socket + if (total_bytes_written) { + _client->send(); + } +} + +void AsyncEventSourceClient::set_max_inflight_bytes(size_t value) { + if (value >= SSE_MIN_INFLIGH && value <= SSE_MAX_INFLIGH) { + _max_inflight = value; + } +} + +/* AsyncEventSource */ + +void AsyncEventSource::authorizeConnect(ArAuthorizeConnectHandler cb) { + AsyncAuthorizationMiddleware *m = new AsyncAuthorizationMiddleware(401, cb); + m->_freeOnRemoval = true; + addMiddleware(m); +} + +void AsyncEventSource::_addClient(AsyncEventSourceClient *client) { + if (!client) { + return; + } +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + _clients.emplace_back(client); + if (_connectcb) { + _connectcb(client); + } + + _adjust_inflight_window(); +} + +void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) { + if (_disconnectcb) { + _disconnectcb(client); + } +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + for (auto i = _clients.begin(); i != _clients.end(); ++i) { + if (i->get() == client) { + _clients.erase(i); + break; + } + } + _adjust_inflight_window(); +} + +void AsyncEventSource::close() { + // While the whole loop is not done, the linked list is locked and so the + // iterator should remain valid even when AsyncEventSource::_handleDisconnect() + // is called very early +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + for (const auto &c : _clients) { + if (c->connected()) { + /** + * @brief: Fix self-deadlock by using recursive_mutex instead. + * Due to c->close() shall call the callback function _onDisconnect() + * The calling flow _onDisconnect() --> _handleDisconnect() --> deadlock + */ + c->close(); + } + } +} + +// pmb fix +size_t AsyncEventSource::avgPacketsWaiting() const { + size_t aql = 0; + uint32_t nConnectedClients = 0; +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + if (!_clients.size()) { + return 0; + } + + for (const auto &c : _clients) { + if (c->connected()) { + aql += c->packetsWaiting(); + ++nConnectedClients; + } + } + return ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up +} + +AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect)); +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + size_t hits = 0; + size_t miss = 0; + for (const auto &c : _clients) { + if (c->write(shared_msg)) { + ++hits; + } else { + ++miss; + } + } + return hits == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED); +} + +size_t AsyncEventSource::count() const { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_client_queue_lock); +#endif + size_t n_clients{0}; + for (const auto &i : _clients) { + if (i->connected()) { + ++n_clients; + } + } + + return n_clients; +} + +bool AsyncEventSource::canHandle(AsyncWebServerRequest *request) const { + return request->isSSE() && request->url().equals(_url); +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { + request->send(new AsyncEventSourceResponse(this)); +} + +void AsyncEventSource::_adjust_inflight_window() { + if (_clients.size()) { + size_t inflight = SSE_MAX_INFLIGH / _clients.size(); + for (const auto &c : _clients) { + c->set_max_inflight_bytes(inflight); + } + // Serial.printf("adjusted inflight to: %u\n", inflight); + } +} + +/* Response */ + +AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) { + _server = server; + _code = 200; + _contentType = T_text_event_stream; + _sendContentLength = false; + addHeader(T_Cache_Control, T_no_cache); + addHeader(T_Connection, T_keep_alive); +} + +void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request) { + String out; + _assembleHead(out, request->version()); + request->client()->write(out.c_str(), _headLength); + _state = RESPONSE_WAIT_ACK; +} + +size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time __attribute__((unused))) { + if (len) { + new AsyncEventSourceClient(request, _server); + } + return 0; +} diff --git a/libraries/ESP_Async_WebServer/src/AsyncEventSource.h b/libraries/ESP_Async_WebServer/src/AsyncEventSource.h new file mode 100644 index 0000000..96f0a89 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncEventSource.h @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNCEVENTSOURCE_H_ +#define ASYNCEVENTSOURCE_H_ + +#include <Arduino.h> + +#ifdef ESP32 +#include <AsyncTCP.h> +#include <mutex> +#ifndef SSE_MAX_QUEUED_MESSAGES +#define SSE_MAX_QUEUED_MESSAGES 32 +#endif +#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets +#define SSE_MAX_INFLIGH 16 * 1024 // but no more than 16k, no need to blow it, since same data is kept in local Q +#elif defined(ESP8266) +#include <ESPAsyncTCP.h> +#ifndef SSE_MAX_QUEUED_MESSAGES +#define SSE_MAX_QUEUED_MESSAGES 8 +#endif +#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets +#define SSE_MAX_INFLIGH 8 * 1024 // but no more than 8k, no need to blow it, since same data is kept in local Q +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include <RPAsyncTCP.h> +#ifndef SSE_MAX_QUEUED_MESSAGES +#define SSE_MAX_QUEUED_MESSAGES 32 +#endif +#define SSE_MIN_INFLIGH 2 * 1460 // allow 2 MSS packets +#define SSE_MAX_INFLIGH 16 * 1024 // but no more than 16k, no need to blow it, since same data is kept in local Q +#endif + +#include <ESPAsyncWebServer.h> + +#ifdef ESP8266 +#include <Hash.h> +#ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library +#include <../src/Hash.h> +#endif +#endif + +class AsyncEventSource; +class AsyncEventSourceResponse; +class AsyncEventSourceClient; +using ArEventHandlerFunction = std::function<void(AsyncEventSourceClient *client)>; +using ArAuthorizeConnectHandler = ArAuthorizeFunction; +// shared message object container +using AsyncEvent_SharedData_t = std::shared_ptr<String>; + +/** + * @brief Async Event Message container with shared message content data + * + */ +class AsyncEventSourceMessage { + +private: + const AsyncEvent_SharedData_t _data; + size_t _sent{0}; // num of bytes already sent + size_t _acked{0}; // num of bytes acked + +public: + AsyncEventSourceMessage(AsyncEvent_SharedData_t data) : _data(data){}; +#if defined(ESP32) + AsyncEventSourceMessage(const char *data, size_t len) : _data(std::make_shared<String>(data, len)){}; +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + AsyncEventSourceMessage(const char *data, size_t len) : _data(std::make_shared<String>()) { + if (data && len > 0) { + _data->concat(data, len); + } + }; +#else + // esp8266's String does not have constructor with data/length arguments. Use a concat method here + AsyncEventSourceMessage(const char *data, size_t len) { + _data->concat(data, len); + }; +#endif + + /** + * @brief acknowledge sending len bytes of data + * @note if num of bytes to ack is larger then the unacknowledged message length the number of carried over bytes are returned + * + * @param len bytes to acknowledge + * @param time + * @return size_t number of extra bytes carried over + */ + size_t ack(size_t len, uint32_t time = 0); + + /** + * @brief write message data to client's buffer + * @note this method does NOT call client's send + * + * @param client + * @return size_t number of bytes written + */ + size_t write(AsyncClient *client); + + /** + * @brief writes message data to client's buffer and calls client's send method + * + * @param client + * @return size_t returns num of bytes the clien was able to send() + */ + size_t send(AsyncClient *client); + + // returns true if full message's length were acked + bool finished() { + return _acked == _data->length(); + } + + /** + * @brief returns true if all data has been sent already + * + */ + bool sent() { + return _sent == _data->length(); + } +}; + +/** + * @brief class holds a sse messages queue for a particular client's connection + * + */ +class AsyncEventSourceClient { +private: + AsyncClient *_client; + AsyncEventSource *_server; + uint32_t _lastId{0}; + size_t _inflight{0}; // num of unacknowledged bytes that has been written to socket buffer + size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer + std::list<AsyncEventSourceMessage> _messageQueue; +#ifdef ESP32 + mutable std::recursive_mutex _lockmq; +#endif + bool _queueMessage(const char *message, size_t len); + bool _queueMessage(AsyncEvent_SharedData_t &&msg); + void _runQueue(); + +public: + AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server); + ~AsyncEventSourceClient(); + + /** + * @brief Send an SSE message to client + * it will craft an SSE message and place it to client's message queue + * + * @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n + * @param event body string, a sinle line string + * @param id sequence id + * @param reconnect client's reconnect timeout + * @return true if message was placed in a queue + * @return false if queue is full + */ + bool send(const char *message, const char *event = NULL, uint32_t id = 0, uint32_t reconnect = 0); + bool send(const String &message, const String &event, uint32_t id = 0, uint32_t reconnect = 0) { + return send(message.c_str(), event.c_str(), id, reconnect); + } + bool send(const String &message, const char *event, uint32_t id = 0, uint32_t reconnect = 0) { + return send(message.c_str(), event, id, reconnect); + } + + /** + * @brief place supplied preformatted SSE message to the message queue + * @note message must a properly formatted SSE string according to https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events + * + * @param message data + * @return true on success + * @return false on queue overflow or no client connected + */ + bool write(AsyncEvent_SharedData_t message) { + return connected() && _queueMessage(std::move(message)); + }; + + [[deprecated("Use _write(AsyncEvent_SharedData_t message) instead to share same data with multiple SSE clients")]] + bool write(const char *message, size_t len) { + return connected() && _queueMessage(message, len); + }; + + // close client's connection + void close(); + + // getters + + AsyncClient *client() { + return _client; + } + bool connected() const { + return _client && _client->connected(); + } + uint32_t lastId() const { + return _lastId; + } + size_t packetsWaiting() const { + return _messageQueue.size(); + }; + + /** + * @brief Sets max amount of bytes that could be written to client's socket while awaiting delivery acknowledge + * used to throttle message delivery length to tradeoff memory consumption + * @note actual amount of data written could possible be a bit larger but no more than available socket buff space + * + * @param value + */ + void set_max_inflight_bytes(size_t value); + + /** + * @brief Get current max inflight bytes value + * + * @return size_t + */ + size_t get_max_inflight_bytes() const { + return _max_inflight; + } + + // system callbacks (do not call if from user code!) + void _onAck(size_t len, uint32_t time); + void _onPoll(); + void _onTimeout(uint32_t time); + void _onDisconnect(); +}; + +/** + * @brief a class that maintains all connected HTTP clients subscribed to SSE delivery + * dispatches supplied messages to the client's queues + * + */ +class AsyncEventSource : public AsyncWebHandler { +private: + String _url; + std::list<std::unique_ptr<AsyncEventSourceClient>> _clients; +#ifdef ESP32 + // Same as for individual messages, protect mutations of _clients list + // since simultaneous access from different tasks is possible + mutable std::recursive_mutex _client_queue_lock; +#endif + ArEventHandlerFunction _connectcb = nullptr; + ArEventHandlerFunction _disconnectcb = nullptr; + + // this method manipulates in-fligh data size for connected client depending on number of active connections + void _adjust_inflight_window(); + +public: + typedef enum { + DISCARDED = 0, + ENQUEUED = 1, + PARTIALLY_ENQUEUED = 2, + } SendStatus; + + AsyncEventSource(const char *url) : _url(url){}; + AsyncEventSource(const String &url) : _url(url){}; + ~AsyncEventSource() { + close(); + }; + + const char *url() const { + return _url.c_str(); + } + // close all connected clients + void close(); + + /** + * @brief set on-connect callback for the client + * used to deliver messages to client on first connect + * + * @param cb + */ + void onConnect(ArEventHandlerFunction cb) { + _connectcb = cb; + } + + /** + * @brief Send an SSE message to client + * it will craft an SSE message and place it to all connected client's message queues + * + * @param message body string, could be single or multi-line string sepprated by \n, \r, \r\n + * @param event body string, a sinle line string + * @param id sequence id + * @param reconnect client's reconnect timeout + * @return SendStatus if message was placed in any/all/part of the client's queues + */ + SendStatus send(const char *message, const char *event = NULL, uint32_t id = 0, uint32_t reconnect = 0); + SendStatus send(const String &message, const String &event, uint32_t id = 0, uint32_t reconnect = 0) { + return send(message.c_str(), event.c_str(), id, reconnect); + } + SendStatus send(const String &message, const char *event, uint32_t id = 0, uint32_t reconnect = 0) { + return send(message.c_str(), event, id, reconnect); + } + + // The client pointer sent to the callback is only for reference purposes. DO NOT CALL ANY METHOD ON IT ! + void onDisconnect(ArEventHandlerFunction cb) { + _disconnectcb = cb; + } + void authorizeConnect(ArAuthorizeConnectHandler cb); + + // returns number of connected clients + size_t count() const; + + // returns average number of messages pending in all client's queues + size_t avgPacketsWaiting() const; + + // system callbacks (do not call from user code!) + void _addClient(AsyncEventSourceClient *client); + void _handleDisconnect(AsyncEventSourceClient *client); + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; +}; + +class AsyncEventSourceResponse : public AsyncWebServerResponse { +private: + AsyncEventSource *_server; + +public: + AsyncEventSourceResponse(AsyncEventSource *server); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { + return true; + } +}; + +#endif /* ASYNCEVENTSOURCE_H_ */ diff --git a/libraries/ESP_Async_WebServer/src/AsyncJson.cpp b/libraries/ESP_Async_WebServer/src/AsyncJson.cpp new file mode 100644 index 0000000..b8d014b --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncJson.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#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<JsonArray>(); + } else { + _root = _jsonBuffer.add<JsonObject>(); + } +} +#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) const { + if (!_onRequest || !request->isHTTP() || !(_method & request->method())) { + return false; + } + + if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) { + return false; + } + + if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_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<JsonVariant>(); +#else + JsonDocument jsonBuffer; + DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as<JsonVariant>(); +#endif + + _onRequest(request, json); + return; + } + } + request->send(_contentLength > _maxContentLength ? 413 : 400); + } else { + request->send(500); + } +} + +void 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) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return; + } + } + if (request->_tempObject != NULL) { + memcpy((uint8_t *)(request->_tempObject) + index, data, len); + } + } +} + +#endif // ASYNC_JSON_SUPPORT diff --git a/libraries/ESP_Async_WebServer/src/AsyncJson.h b/libraries/ESP_Async_WebServer/src/AsyncJson.h new file mode 100644 index 0000000..b5777d6 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncJson.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNC_JSON_H_ +#define ASYNC_JSON_H_ + +#if __has_include("ArduinoJson.h") +#include <ArduinoJson.h> +#if ARDUINOJSON_VERSION_MAJOR >= 5 +#define ASYNC_JSON_SUPPORT 1 +#else +#define ASYNC_JSON_SUPPORT 0 +#endif // ARDUINOJSON_VERSION_MAJOR >= 5 +#endif // __has_include("ArduinoJson.h") + +#if ASYNC_JSON_SUPPORT == 1 +#include <ESPAsyncWebServer.h> + +#include "ChunkPrint.h" + +#if ARDUINOJSON_VERSION_MAJOR == 6 +#ifndef DYNAMIC_JSON_DOCUMENT_SIZE +#define DYNAMIC_JSON_DOCUMENT_SIZE 1024 +#endif +#endif + +class AsyncJsonResponse : public AsyncAbstractResponse { +protected: +#if ARDUINOJSON_VERSION_MAJOR == 5 + DynamicJsonBuffer _jsonBuffer; +#elif ARDUINOJSON_VERSION_MAJOR == 6 + DynamicJsonDocument _jsonBuffer; +#else + JsonDocument _jsonBuffer; +#endif + + JsonVariant _root; + bool _isValid; + +public: +#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(); + size_t getSize() const { + return _jsonBuffer.size(); + } + size_t _fillBuffer(uint8_t *data, size_t len); +#if ARDUINOJSON_VERSION_MAJOR >= 6 + bool overflowed() const { + return _jsonBuffer.overflowed(); + } +#endif +}; + +class PrettyAsyncJsonResponse : public AsyncJsonResponse { +public: +#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<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction; + +class AsyncCallbackJsonWebHandler : public AsyncWebHandler { +protected: + String _uri; + WebRequestMethodComposite _method; + ArJsonRequestHandlerFunction _onRequest; + size_t _contentLength; +#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); +#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; + } + + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; + void handleUpload( + __unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len, + __unused bool final + ) override final {} + void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final; + bool isRequestHandlerTrivial() const override final { + return !_onRequest; + } +}; + +#endif // ASYNC_JSON_SUPPORT == 1 + +#endif // ASYNC_JSON_H_ diff --git a/libraries/ESP_Async_WebServer/src/AsyncMessagePack.cpp b/libraries/ESP_Async_WebServer/src/AsyncMessagePack.cpp new file mode 100644 index 0000000..0c6faa1 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncMessagePack.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "AsyncMessagePack.h" + +#if ASYNC_MSG_PACK_SUPPORT == 1 + +#if ARDUINOJSON_VERSION_MAJOR == 6 +AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { + _code = 200; + _contentType = asyncsrv::T_application_msgpack; + if (isArray) { + _root = _jsonBuffer.createNestedArray(); + } else { + _root = _jsonBuffer.createNestedObject(); + } +} +#else +AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray) : _isValid{false} { + _code = 200; + _contentType = asyncsrv::T_application_msgpack; + if (isArray) { + _root = _jsonBuffer.add<JsonArray>(); + } else { + _root = _jsonBuffer.add<JsonObject>(); + } +} +#endif + +size_t AsyncMessagePackResponse::setLength() { + _contentLength = measureMsgPack(_root); + if (_contentLength) { + _isValid = true; + } + return _contentLength; +} + +size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) { + ChunkPrint dest(data, _sentLength, len); + serializeMsgPack(_root, dest); + return len; +} + +#if ARDUINOJSON_VERSION_MAJOR == 6 +AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler( + const String &uri, ArMessagePackRequestHandlerFunction onRequest, size_t maxJsonBufferSize +) + : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {} +#else +AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest) + : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} +#endif + +bool AsyncCallbackMessagePackWebHandler::canHandle(AsyncWebServerRequest *request) const { + if (!_onRequest || !request->isHTTP() || !(_method & request->method())) { + return false; + } + + if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) { + return false; + } + + if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)) { + return false; + } + + return true; +} + +void AsyncCallbackMessagePackWebHandler::handleRequest(AsyncWebServerRequest *request) { + if (_onRequest) { + if (request->method() == HTTP_GET) { + JsonVariant json; + _onRequest(request, json); + return; + } else if (request->_tempObject != NULL) { + +#if ARDUINOJSON_VERSION_MAJOR == 6 + DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); + DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as<JsonVariant>(); +#else + JsonDocument jsonBuffer; + DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject)); + if (!error) { + JsonVariant json = jsonBuffer.as<JsonVariant>(); +#endif + + _onRequest(request, json); + return; + } + } + request->send(_contentLength > _maxContentLength ? 413 : 400); + } else { + request->send(500); + } +} + +void AsyncCallbackMessagePackWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (_onRequest) { + _contentLength = total; + if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { + request->_tempObject = malloc(total); + if (request->_tempObject == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return; + } + } + if (request->_tempObject != NULL) { + memcpy((uint8_t *)(request->_tempObject) + index, data, len); + } + } +} + +#endif // ASYNC_MSG_PACK_SUPPORT diff --git a/libraries/ESP_Async_WebServer/src/AsyncMessagePack.h b/libraries/ESP_Async_WebServer/src/AsyncMessagePack.h new file mode 100644 index 0000000..7488b5c --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncMessagePack.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#pragma once + +/* + server.on("/msg_pack", HTTP_ANY, [](AsyncWebServerRequest * request) { + AsyncMessagePackResponse * response = new AsyncMessagePackResponse(); + JsonObject& root = response->getRoot(); + root["key1"] = "key number one"; + JsonObject& nested = root.createNestedObject("nested"); + nested["key1"] = "key number one"; + response->setLength(); + request->send(response); + }); + + -------------------- + + AsyncCallbackMessagePackWebHandler* handler = new AsyncCallbackMessagePackWebHandler("/msg_pack/endpoint"); + handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { + JsonObject jsonObj = json.as<JsonObject>(); + // ... + }); + server.addHandler(handler); +*/ + +#if __has_include("ArduinoJson.h") +#include <ArduinoJson.h> +#if ARDUINOJSON_VERSION_MAJOR >= 6 +#define ASYNC_MSG_PACK_SUPPORT 1 +#else +#define ASYNC_MSG_PACK_SUPPORT 0 +#endif // ARDUINOJSON_VERSION_MAJOR >= 6 +#endif // __has_include("ArduinoJson.h") + +#if ASYNC_MSG_PACK_SUPPORT == 1 +#include <ESPAsyncWebServer.h> + +#include "ChunkPrint.h" + +#if ARDUINOJSON_VERSION_MAJOR == 6 +#ifndef DYNAMIC_JSON_DOCUMENT_SIZE +#define DYNAMIC_JSON_DOCUMENT_SIZE 1024 +#endif +#endif + +class AsyncMessagePackResponse : public AsyncAbstractResponse { +protected: +#if ARDUINOJSON_VERSION_MAJOR == 6 + DynamicJsonDocument _jsonBuffer; +#else + JsonDocument _jsonBuffer; +#endif + + JsonVariant _root; + bool _isValid; + +public: +#if ARDUINOJSON_VERSION_MAJOR == 6 + AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); +#else + AsyncMessagePackResponse(bool isArray = false); +#endif + JsonVariant &getRoot() { + return _root; + } + bool _sourceValid() const { + return _isValid; + } + size_t setLength(); + size_t getSize() const { + return _jsonBuffer.size(); + } + size_t _fillBuffer(uint8_t *data, size_t len); +#if ARDUINOJSON_VERSION_MAJOR >= 6 + bool overflowed() const { + return _jsonBuffer.overflowed(); + } +#endif +}; + +typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArMessagePackRequestHandlerFunction; + +class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler { +protected: + String _uri; + WebRequestMethodComposite _method; + ArMessagePackRequestHandlerFunction _onRequest; + size_t _contentLength; +#if ARDUINOJSON_VERSION_MAJOR == 6 + size_t maxJsonBufferSize; +#endif + size_t _maxContentLength; + +public: +#if ARDUINOJSON_VERSION_MAJOR == 6 + AsyncCallbackMessagePackWebHandler( + const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE + ); +#else + AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr); +#endif + + void setMethod(WebRequestMethodComposite method) { + _method = method; + } + void setMaxContentLength(int maxContentLength) { + _maxContentLength = maxContentLength; + } + void onRequest(ArMessagePackRequestHandlerFunction fn) { + _onRequest = fn; + } + + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; + void handleUpload( + __unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len, + __unused bool final + ) override final {} + void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final; + bool isRequestHandlerTrivial() const override final { + return !_onRequest; + } +}; + +#endif // ASYNC_MSG_PACK_SUPPORT == 1 diff --git a/libraries/ESP_Async_WebServer/src/AsyncWebHeader.cpp b/libraries/ESP_Async_WebServer/src/AsyncWebHeader.cpp new file mode 100644 index 0000000..6d82f74 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncWebHeader.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include <ESPAsyncWebServer.h> + +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; + if (str.reserve(_name.length() + _value.length() + 2)) { + str.concat(_name); + str.concat((char)0x3a); + str.concat((char)0x20); + str.concat(_value); + str.concat(asyncsrv::T_rn); + } else { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + } + return str; +} diff --git a/libraries/ESP_Async_WebServer/src/AsyncWebServerVersion.h b/libraries/ESP_Async_WebServer/src/AsyncWebServerVersion.h new file mode 100644 index 0000000..0bfa5a0 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncWebServerVersion.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** Major version number (X.x.x) */ +#define ASYNCWEBSERVER_VERSION_MAJOR 3 +/** Minor version number (x.X.x) */ +#define ASYNCWEBSERVER_VERSION_MINOR 7 +/** Patch version number (x.x.X) */ +#define ASYNCWEBSERVER_VERSION_PATCH 7 + +/** + * Macro to convert version number into an integer + * + * To be used in comparisons, such as ASYNCWEBSERVER_VERSION >= ASYNCWEBSERVER_VERSION_VAL(2, 0, 0) + */ +#define ASYNCWEBSERVER_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) + +/** + * Current version, as an integer + * + * To be used in comparisons, such as ASYNCWEBSERVER_VERSION_NUM >= ASYNCWEBSERVER_VERSION_VAL(2, 0, 0) + */ +#define ASYNCWEBSERVER_VERSION_NUM ASYNCWEBSERVER_VERSION_VAL(ASYNCWEBSERVER_VERSION_MAJOR, ASYNCWEBSERVER_VERSION_MINOR, ASYNCWEBSERVER_VERSION_PATCH) + +/** + * Current version, as string + */ +#define df2xstr(s) #s +#define df2str(s) df2xstr(s) +#define ASYNCWEBSERVER_VERSION df2str(ASYNCWEBSERVER_VERSION_MAJOR) "." df2str(ASYNCWEBSERVER_VERSION_MINOR) "." df2str(ASYNCWEBSERVER_VERSION_PATCH) + +#ifdef __cplusplus +} +#endif diff --git a/libraries/ESP_Async_WebServer/src/AsyncWebSocket.cpp b/libraries/ESP_Async_WebServer/src/AsyncWebSocket.cpp new file mode 100644 index 0000000..f86d616 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncWebSocket.cpp @@ -0,0 +1,1364 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "AsyncWebSocket.h" +#include "Arduino.h" + +#include <cstring> + +#include <libb64/cencode.h> + +#if defined(ESP32) +#if ESP_IDF_VERSION_MAJOR < 5 +#include "BackPort_SHA1Builder.h" +#else +#include <SHA1Builder.h> +#endif +#include <rom/ets_sys.h> +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266) +#include <Hash.h> +#endif + +using namespace asyncsrv; + +size_t webSocketSendFrameWindow(AsyncClient *client) { + if (!client || !client->canSend()) { + return 0; + } + size_t space = client->space(); + if (space < 9) { + return 0; + } + return space - 8; +} + +size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool mask, uint8_t *data, size_t len) { + if (!client || !client->canSend()) { + // Serial.println("SF 1"); + return 0; + } + size_t space = client->space(); + if (space < 2) { + // Serial.println("SF 2"); + return 0; + } + uint8_t mbuf[4] = {0, 0, 0, 0}; + uint8_t headLen = 2; + if (len && mask) { + headLen += 4; + mbuf[0] = rand() % 0xFF; + mbuf[1] = rand() % 0xFF; + mbuf[2] = rand() % 0xFF; + mbuf[3] = rand() % 0xFF; + } + if (len > 125) { + headLen += 2; + } + if (space < headLen) { + // Serial.println("SF 2"); + return 0; + } + space -= headLen; + + if (len > space) { + len = space; + } + + uint8_t *buf = (uint8_t *)malloc(headLen); + if (buf == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + client->abort(); + return 0; + } + + buf[0] = opcode & 0x0F; + if (final) { + buf[0] |= 0x80; + } + if (len < 126) { + buf[1] = len & 0x7F; + } else { + buf[1] = 126; + buf[2] = (uint8_t)((len >> 8) & 0xFF); + buf[3] = (uint8_t)(len & 0xFF); + } + if (len && mask) { + buf[1] |= 0x80; + memcpy(buf + (headLen - 4), mbuf, 4); + } + if (client->add((const char *)buf, headLen) != headLen) { + // os_printf("error adding %lu header bytes\n", headLen); + free(buf); + // Serial.println("SF 4"); + return 0; + } + free(buf); + + if (len) { + if (len && mask) { + size_t i; + for (i = 0; i < len; i++) { + data[i] = data[i] ^ mbuf[i % 4]; + } + } + if (client->add((const char *)data, len) != len) { + // os_printf("error adding %lu data bytes\n", len); + // Serial.println("SF 5"); + return 0; + } + } + if (!client->send()) { + // os_printf("error sending frame: %lu\n", headLen+len); + // Serial.println("SF 6"); + return 0; + } + // Serial.println("SF"); + return len; +} + +/* + * AsyncWebSocketMessageBuffer + */ + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(const uint8_t *data, size_t size) : _buffer(std::make_shared<std::vector<uint8_t>>(size)) { + if (_buffer->capacity() < size) { + _buffer->reserve(size); + } else { + std::memcpy(_buffer->data(), data, size); + } +} + +AsyncWebSocketMessageBuffer::AsyncWebSocketMessageBuffer(size_t size) : _buffer(std::make_shared<std::vector<uint8_t>>(size)) { + if (_buffer->capacity() < size) { + _buffer->reserve(size); + } +} + +bool AsyncWebSocketMessageBuffer::reserve(size_t size) { + if (_buffer->capacity() >= size) { + return true; + } + _buffer->reserve(size); + return _buffer->capacity() >= size; +} + +/* + * Control Frame + */ + +class AsyncWebSocketControl { +private: + uint8_t _opcode; + uint8_t *_data; + size_t _len; + bool _mask; + bool _finished; + +public: + AsyncWebSocketControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false) + : _opcode(opcode), _len(len), _mask(len && mask), _finished(false) { + if (data == NULL) { + _len = 0; + } + if (_len) { + if (_len > 125) { + _len = 125; + } + + _data = (uint8_t *)malloc(_len); + + if (_data == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + _len = 0; + } else { + memcpy(_data, data, len); + } + } else { + _data = NULL; + } + } + + ~AsyncWebSocketControl() { + if (_data != NULL) { + free(_data); + } + } + + bool finished() const { + return _finished; + } + uint8_t opcode() { + return _opcode; + } + uint8_t len() { + return _len + 2; + } + size_t send(AsyncClient *client) { + _finished = true; + return webSocketSendFrame(client, true, _opcode & 0x0F, _mask, _data, _len); + } +}; + +/* + * AsyncWebSocketMessage Message + */ + +AsyncWebSocketMessage::AsyncWebSocketMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode, bool mask) + : _WSbuffer{buffer}, _opcode(opcode & 0x07), _mask{mask}, _status{_WSbuffer ? WS_MSG_SENDING : WS_MSG_ERROR} {} + +void AsyncWebSocketMessage::ack(size_t len, uint32_t time) { + (void)time; + _acked += len; + if (_sent >= _WSbuffer->size() && _acked >= _ack) { + _status = WS_MSG_SENT; + } + // ets_printf("A: %u\n", len); +} + +size_t AsyncWebSocketMessage::send(AsyncClient *client) { + if (!client) { + return 0; + } + + if (_status != WS_MSG_SENDING) { + return 0; + } + if (_acked < _ack) { + return 0; + } + if (_sent == _WSbuffer->size()) { + if (_acked == _ack) { + _status = WS_MSG_SENT; + } + return 0; + } + if (_sent > _WSbuffer->size()) { + _status = WS_MSG_ERROR; + // ets_printf("E: %u > %u\n", _sent, _WSbuffer->length()); + return 0; + } + + size_t toSend = _WSbuffer->size() - _sent; + size_t window = webSocketSendFrameWindow(client); + + if (window < toSend) { + toSend = window; + } + + _sent += toSend; + _ack += toSend + ((toSend < 126) ? 2 : 4) + (_mask * 4); + + // ets_printf("W: %u %u\n", _sent - toSend, toSend); + + bool final = (_sent == _WSbuffer->size()); + uint8_t *dPtr = (uint8_t *)(_WSbuffer->data() + (_sent - toSend)); + uint8_t opCode = (toSend && _sent == toSend) ? _opcode : (uint8_t)WS_CONTINUATION; + + size_t sent = webSocketSendFrame(client, final, opCode, _mask, dPtr, toSend); + _status = WS_MSG_SENDING; + if (toSend && sent != toSend) { + // ets_printf("E: %u != %u\n", toSend, sent); + _sent -= (toSend - sent); + _ack -= (toSend - sent); + } + // ets_printf("S: %u %u\n", _sent, sent); + return sent; +} + +/* + * Async WebSocket Client + */ +const char *AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING"; +const size_t AWSC_PING_PAYLOAD_LEN = 22; + +AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : _tempObject(NULL) { + _client = request->client(); + _server = server; + _clientId = _server->_getNextId(); + _status = WS_CONNECTED; + _pstate = 0; + _lastMessageTime = millis(); + _keepAlivePeriod = 0; + _client->setRxTimeout(0); + _client->onError( + [](void *r, AsyncClient *c, int8_t error) { + (void)c; + ((AsyncWebSocketClient *)(r))->_onError(error); + }, + this + ); + _client->onAck( + [](void *r, AsyncClient *c, size_t len, uint32_t time) { + (void)c; + ((AsyncWebSocketClient *)(r))->_onAck(len, time); + }, + this + ); + _client->onDisconnect( + [](void *r, AsyncClient *c) { + ((AsyncWebSocketClient *)(r))->_onDisconnect(); + delete c; + }, + this + ); + _client->onTimeout( + [](void *r, AsyncClient *c, uint32_t time) { + (void)c; + ((AsyncWebSocketClient *)(r))->_onTimeout(time); + }, + this + ); + _client->onData( + [](void *r, AsyncClient *c, void *buf, size_t len) { + (void)c; + ((AsyncWebSocketClient *)(r))->_onData(buf, len); + }, + this + ); + _client->onPoll( + [](void *r, AsyncClient *c) { + (void)c; + ((AsyncWebSocketClient *)(r))->_onPoll(); + }, + this + ); + delete request; + memset(&_pinfo, 0, sizeof(_pinfo)); +} + +AsyncWebSocketClient::~AsyncWebSocketClient() { + { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lock); +#endif + _messageQueue.clear(); + _controlQueue.clear(); + } + _server->_handleEvent(this, WS_EVT_DISCONNECT, NULL, NULL, 0); +} + +void AsyncWebSocketClient::_clearQueue() { + while (!_messageQueue.empty() && _messageQueue.front().finished()) { + _messageQueue.pop_front(); + } +} + +void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { + _lastMessageTime = millis(); + +#ifdef ESP32 + std::unique_lock<std::recursive_mutex> lock(_lock); +#endif + + if (!_controlQueue.empty()) { + auto &head = _controlQueue.front(); + if (head.finished()) { + len -= head.len(); + if (_status == WS_DISCONNECTING && head.opcode() == WS_DISCONNECT) { + _controlQueue.pop_front(); + _status = WS_DISCONNECTED; + if (_client) { +#ifdef ESP32 + /* + Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. + Due to _client->close(true) shall call the callback function _onDisconnect() + The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() + */ + lock.unlock(); +#endif + _client->close(true); + } + return; + } + _controlQueue.pop_front(); + } + } + + if (len && !_messageQueue.empty()) { + _messageQueue.front().ack(len, time); + } + + _clearQueue(); + + _runQueue(); +} + +void AsyncWebSocketClient::_onPoll() { + if (!_client) { + return; + } + +#ifdef ESP32 + std::unique_lock<std::recursive_mutex> lock(_lock); +#endif + if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { + _runQueue(); + } else if (_keepAlivePeriod > 0 && (millis() - _lastMessageTime) >= _keepAlivePeriod && (_controlQueue.empty() && _messageQueue.empty())) { +#ifdef ESP32 + lock.unlock(); +#endif + ping((uint8_t *)AWSC_PING_PAYLOAD, AWSC_PING_PAYLOAD_LEN); + } +} + +void AsyncWebSocketClient::_runQueue() { + // all calls to this method MUST be protected by a mutex lock! + if (!_client) { + return; + } + + _clearQueue(); + + if (!_controlQueue.empty() && (_messageQueue.empty() || _messageQueue.front().betweenFrames()) + && webSocketSendFrameWindow(_client) > (size_t)(_controlQueue.front().len() - 1)) { + _controlQueue.front().send(_client); + } else if (!_messageQueue.empty() && _messageQueue.front().betweenFrames() && webSocketSendFrameWindow(_client)) { + _messageQueue.front().send(_client); + } +} + +bool AsyncWebSocketClient::queueIsFull() const { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lock); +#endif + return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); +} + +size_t AsyncWebSocketClient::queueLen() const { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lock); +#endif + return _messageQueue.size(); +} + +bool AsyncWebSocketClient::canSend() const { +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lock); +#endif + return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; +} + +bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, size_t len, bool mask) { + if (!_client) { + return false; + } + +#ifdef ESP32 + std::lock_guard<std::recursive_mutex> lock(_lock); +#endif + + _controlQueue.emplace_back(opcode, data, len, mask); + + if (_client && _client->canSend()) { + _runQueue(); + } + + return true; +} + +bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode, bool mask) { + if (!_client || buffer->size() == 0 || _status != WS_CONNECTED) { + return false; + } + +#ifdef ESP32 + std::unique_lock<std::recursive_mutex> lock(_lock); +#endif + + if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { + if (closeWhenFull) { + _status = WS_DISCONNECTED; + + if (_client) { +#ifdef ESP32 + /* + Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. + Due to _client->close(true) shall call the callback function _onDisconnect() + The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient() + */ + lock.unlock(); +#endif + _client->close(true); + } + +#ifdef ESP8266 + ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: closing connection\n"); +#elif defined(ESP32) + log_e("Too many messages queued: closing connection"); +#endif + + } else { +#ifdef ESP8266 + ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: discarding new message\n"); +#elif defined(ESP32) + log_e("Too many messages queued: discarding new message"); +#endif + } + + return false; + } + + _messageQueue.emplace_back(buffer, opcode, mask); + + if (_client && _client->canSend()) { + _runQueue(); + } + + return true; +} + +void AsyncWebSocketClient::close(uint16_t code, const char *message) { + if (_status != WS_CONNECTED) { + return; + } + + _status = WS_DISCONNECTING; + + if (code) { + uint8_t packetLen = 2; + if (message != NULL) { + size_t mlen = strlen(message); + if (mlen > 123) { + mlen = 123; + } + packetLen += mlen; + } + char *buf = (char *)malloc(packetLen); + if (buf != NULL) { + buf[0] = (uint8_t)(code >> 8); + buf[1] = (uint8_t)(code & 0xFF); + if (message != NULL) { + memcpy(buf + 2, message, packetLen - 2); + } + _queueControl(WS_DISCONNECT, (uint8_t *)buf, packetLen); + free(buf); + return; + } else { +#ifdef ESP32 + log_e("Failed to allocate"); + _client->abort(); +#endif + } + } + _queueControl(WS_DISCONNECT); +} + +bool AsyncWebSocketClient::ping(const uint8_t *data, size_t len) { + return _status == WS_CONNECTED && _queueControl(WS_PING, data, len); +} + +void AsyncWebSocketClient::_onError(int8_t) { + // Serial.println("onErr"); +} + +void AsyncWebSocketClient::_onTimeout(uint32_t time) { + if (!_client) { + return; + } + // Serial.println("onTime"); + (void)time; + _client->close(true); +} + +void AsyncWebSocketClient::_onDisconnect() { + // Serial.println("onDis"); + _client = nullptr; + _server->_handleDisconnect(this); +} + +void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { + _lastMessageTime = millis(); + uint8_t *data = (uint8_t *)pbuf; + while (plen > 0) { + if (!_pstate) { + const uint8_t *fdata = data; + + _pinfo.index = 0; + _pinfo.final = (fdata[0] & 0x80) != 0; + _pinfo.opcode = fdata[0] & 0x0F; + _pinfo.masked = (fdata[1] & 0x80) != 0; + _pinfo.len = fdata[1] & 0x7F; + + // log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen); + // log_d("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status); + // log_d("WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len); + + data += 2; + plen -= 2; + + if (_pinfo.len == 126 && plen >= 2) { + _pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8; + data += 2; + plen -= 2; + + } else if (_pinfo.len == 127 && plen >= 8) { + _pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 + | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56; + data += 8; + plen -= 8; + } + + if (_pinfo.masked + && plen >= 4) { // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not decrement plen which is already 0. + memcpy(_pinfo.mask, data, 4); + data += 4; + plen -= 4; + } + } + + const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); + const auto datalast = data[datalen]; + + if (_pinfo.masked) { + for (size_t i = 0; i < datalen; i++) { + data[i] ^= _pinfo.mask[(_pinfo.index + i) % 4]; + } + } + + if ((datalen + _pinfo.index) < _pinfo.len) { + _pstate = 1; + + if (_pinfo.index == 0) { + if (_pinfo.opcode) { + _pinfo.message_opcode = _pinfo.opcode; + _pinfo.num = 0; + } + } + if (datalen > 0) { + _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); + } + + _pinfo.index += datalen; + } else if ((datalen + _pinfo.index) == _pinfo.len) { + _pstate = 0; + if (_pinfo.opcode == WS_DISCONNECT) { + if (datalen) { + uint16_t reasonCode = (uint16_t)(data[0] << 8) + data[1]; + char *reasonString = (char *)(data + 2); + if (reasonCode > 1001) { + _server->_handleEvent(this, WS_EVT_ERROR, (void *)&reasonCode, (uint8_t *)reasonString, strlen(reasonString)); + } + } + if (_status == WS_DISCONNECTING) { + _status = WS_DISCONNECTED; + if (_client) { + _client->close(true); + } + } else { + _status = WS_DISCONNECTING; + if (_client) { + _client->ackLater(); + } + _queueControl(WS_DISCONNECT, data, datalen); + } + } else if (_pinfo.opcode == WS_PING) { + _server->_handleEvent(this, WS_EVT_PING, NULL, NULL, 0); + _queueControl(WS_PONG, data, datalen); + } else if (_pinfo.opcode == WS_PONG) { + if (datalen != AWSC_PING_PAYLOAD_LEN || memcmp(AWSC_PING_PAYLOAD, data, AWSC_PING_PAYLOAD_LEN) != 0) { + _server->_handleEvent(this, WS_EVT_PONG, NULL, NULL, 0); + } + } else if (_pinfo.opcode < WS_DISCONNECT) { // continuation or text/binary frame + _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); + if (_pinfo.final) { + _pinfo.num = 0; + } else { + _pinfo.num += 1; + } + } + } else { + // os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); + // what should we do? + break; + } + + // restore byte as _handleEvent may have added a null terminator i.e., data[len] = 0; + if (datalen) { + data[datalen] = datalast; + } + + data += datalen; + plen -= datalen; + } +} + +size_t AsyncWebSocketClient::printf(const char *format, ...) { + va_list arg; + va_start(arg, format); + size_t len = vsnprintf(nullptr, 0, format, arg); + va_end(arg); + + if (len == 0) { + return 0; + } + + char *buffer = new char[len + 1]; + + if (!buffer) { + return 0; + } + + va_start(arg, format); + len = vsnprintf(buffer, len + 1, format, arg); + va_end(arg); + + bool enqueued = text(buffer, len); + delete[] buffer; + return enqueued ? len : 0; +} + +#ifdef ESP8266 +size_t AsyncWebSocketClient::printf_P(PGM_P formatP, ...) { + va_list arg; + va_start(arg, formatP); + size_t len = vsnprintf_P(nullptr, 0, formatP, arg); + va_end(arg); + + if (len == 0) { + return 0; + } + + char *buffer = new char[len + 1]; + + if (!buffer) { + return 0; + } + + va_start(arg, formatP); + len = vsnprintf_P(buffer, len + 1, formatP, arg); + va_end(arg); + + bool enqueued = text(buffer, len); + delete[] buffer; + return enqueued ? len : 0; +} +#endif + +namespace { +AsyncWebSocketSharedBuffer makeSharedBuffer(const uint8_t *message, size_t len) { + auto buffer = std::make_shared<std::vector<uint8_t>>(len); + std::memcpy(buffer->data(), message, len); + return buffer; +} +} // namespace + +bool AsyncWebSocketClient::text(AsyncWebSocketMessageBuffer *buffer) { + bool enqueued = false; + if (buffer) { + enqueued = text(std::move(buffer->_buffer)); + delete buffer; + } + return enqueued; +} + +bool AsyncWebSocketClient::text(AsyncWebSocketSharedBuffer buffer) { + return _queueMessage(buffer); +} + +bool AsyncWebSocketClient::text(const uint8_t *message, size_t len) { + return text(makeSharedBuffer(message, len)); +} + +bool AsyncWebSocketClient::text(const char *message, size_t len) { + return text((const uint8_t *)message, len); +} + +bool AsyncWebSocketClient::text(const char *message) { + return text(message, strlen(message)); +} + +bool AsyncWebSocketClient::text(const String &message) { + return text(message.c_str(), message.length()); +} + +#ifdef ESP8266 +bool AsyncWebSocketClient::text(const __FlashStringHelper *data) { + PGM_P p = reinterpret_cast<PGM_P>(data); + + size_t n = 0; + while (1) { + if (pgm_read_byte(p + n) == 0) { + break; + } + n += 1; + } + + char *message = (char *)malloc(n + 1); + bool enqueued = false; + if (message) { + memcpy_P(message, p, n); + message[n] = 0; + enqueued = text(message, n); + free(message); + } + return enqueued; +} +#endif // ESP8266 + +bool AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer *buffer) { + bool enqueued = false; + if (buffer) { + enqueued = binary(std::move(buffer->_buffer)); + delete buffer; + } + return enqueued; +} + +bool AsyncWebSocketClient::binary(AsyncWebSocketSharedBuffer buffer) { + return _queueMessage(buffer, WS_BINARY); +} + +bool AsyncWebSocketClient::binary(const uint8_t *message, size_t len) { + return binary(makeSharedBuffer(message, len)); +} + +bool AsyncWebSocketClient::binary(const char *message, size_t len) { + return binary((const uint8_t *)message, len); +} + +bool AsyncWebSocketClient::binary(const char *message) { + return binary(message, strlen(message)); +} + +bool AsyncWebSocketClient::binary(const String &message) { + return binary(message.c_str(), message.length()); +} + +#ifdef ESP8266 +bool AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) { + PGM_P p = reinterpret_cast<PGM_P>(data); + char *message = (char *)malloc(len); + bool enqueued = false; + if (message) { + memcpy_P(message, p, len); + enqueued = binary(message, len); + free(message); + } + return enqueued; +} +#endif + +IPAddress AsyncWebSocketClient::remoteIP() const { + if (!_client) { + return IPAddress((uint32_t)0U); + } + + return _client->remoteIP(); +} + +uint16_t AsyncWebSocketClient::remotePort() const { + if (!_client) { + return 0; + } + + return _client->remotePort(); +} + +/* + * Async Web Socket - Each separate socket location + */ + +void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + if (_eventHandler != NULL) { + _eventHandler(this, client, type, arg, data, len); + } +} + +AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) { + _clients.emplace_back(request, this); + _handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0); + return &_clients.back(); +} + +void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) { + const auto client_id = client->id(); + const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [client_id](const AsyncWebSocketClient &c) { + return c.id() == client_id; + }); + if (iter != std::end(_clients)) { + _clients.erase(iter); + } +} + +bool AsyncWebSocket::availableForWriteAll() { + return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { + return c.queueIsFull(); + }); +} + +bool AsyncWebSocket::availableForWrite(uint32_t id) { + const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [id](const AsyncWebSocketClient &c) { + return c.id() == id; + }); + if (iter == std::end(_clients)) { + return true; + } + return !iter->queueIsFull(); +} + +size_t AsyncWebSocket::count() const { + return std::count_if(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { + return c.status() == WS_CONNECTED; + }); +} + +AsyncWebSocketClient *AsyncWebSocket::client(uint32_t id) { + const auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const AsyncWebSocketClient &c) { + return c.id() == id && c.status() == WS_CONNECTED; + }); + if (iter == std::end(_clients)) { + return nullptr; + } + + return &(*iter); +} + +void AsyncWebSocket::close(uint32_t id, uint16_t code, const char *message) { + if (AsyncWebSocketClient *c = client(id)) { + c->close(code, message); + } +} + +void AsyncWebSocket::closeAll(uint16_t code, const char *message) { + for (auto &c : _clients) { + if (c.status() == WS_CONNECTED) { + c.close(code, message); + } + } +} + +void AsyncWebSocket::cleanupClients(uint16_t maxClients) { + if (count() > maxClients) { + _clients.front().close(); + } + + for (auto i = _clients.begin(); i != _clients.end(); ++i) { + if (i->shouldBeDeleted()) { + _clients.erase(i); + break; + } + } +} + +bool AsyncWebSocket::ping(uint32_t id, const uint8_t *data, size_t len) { + AsyncWebSocketClient *c = client(id); + return c && c->ping(data, len); +} + +AsyncWebSocket::SendStatus AsyncWebSocket::pingAll(const uint8_t *data, size_t len) { + size_t hit = 0; + size_t miss = 0; + for (auto &c : _clients) { + if (c.status() == WS_CONNECTED && c.ping(data, len)) { + hit++; + } else { + miss++; + } + } + return hit == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED); +} + +bool AsyncWebSocket::text(uint32_t id, const uint8_t *message, size_t len) { + AsyncWebSocketClient *c = client(id); + return c && c->text(makeSharedBuffer(message, len)); +} +bool AsyncWebSocket::text(uint32_t id, const char *message, size_t len) { + return text(id, (const uint8_t *)message, len); +} +bool AsyncWebSocket::text(uint32_t id, const char *message) { + return text(id, message, strlen(message)); +} +bool AsyncWebSocket::text(uint32_t id, const String &message) { + return text(id, message.c_str(), message.length()); +} + +#ifdef ESP8266 +bool AsyncWebSocket::text(uint32_t id, const __FlashStringHelper *data) { + PGM_P p = reinterpret_cast<PGM_P>(data); + + size_t n = 0; + while (true) { + if (pgm_read_byte(p + n) == 0) { + break; + } + n += 1; + } + + char *message = (char *)malloc(n + 1); + bool enqueued = false; + if (message) { + memcpy_P(message, p, n); + message[n] = 0; + enqueued = text(id, message, n); + free(message); + } + return enqueued; +} +#endif // ESP8266 + +bool AsyncWebSocket::text(uint32_t id, AsyncWebSocketMessageBuffer *buffer) { + bool enqueued = false; + if (buffer) { + enqueued = text(id, std::move(buffer->_buffer)); + delete buffer; + } + return enqueued; +} +bool AsyncWebSocket::text(uint32_t id, AsyncWebSocketSharedBuffer buffer) { + AsyncWebSocketClient *c = client(id); + return c && c->text(buffer); +} + +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(const uint8_t *message, size_t len) { + return textAll(makeSharedBuffer(message, len)); +} +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(const char *message, size_t len) { + return textAll((const uint8_t *)message, len); +} +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(const char *message) { + return textAll(message, strlen(message)); +} +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(const String &message) { + return textAll(message.c_str(), message.length()); +} +#ifdef ESP8266 +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(const __FlashStringHelper *data) { + PGM_P p = reinterpret_cast<PGM_P>(data); + + size_t n = 0; + while (1) { + if (pgm_read_byte(p + n) == 0) { + break; + } + n += 1; + } + + char *message = (char *)malloc(n + 1); + AsyncWebSocket::SendStatus status = DISCARDED; + if (message) { + memcpy_P(message, p, n); + message[n] = 0; + status = textAll(message, n); + free(message); + } + return status; +} +#endif // ESP8266 +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(AsyncWebSocketMessageBuffer *buffer) { + AsyncWebSocket::SendStatus status = DISCARDED; + if (buffer) { + status = textAll(std::move(buffer->_buffer)); + delete buffer; + } + return status; +} + +AsyncWebSocket::SendStatus AsyncWebSocket::textAll(AsyncWebSocketSharedBuffer buffer) { + size_t hit = 0; + size_t miss = 0; + for (auto &c : _clients) { + if (c.status() == WS_CONNECTED && c.text(buffer)) { + hit++; + } else { + miss++; + } + } + return hit == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED); +} + +bool AsyncWebSocket::binary(uint32_t id, const uint8_t *message, size_t len) { + AsyncWebSocketClient *c = client(id); + return c && c->binary(makeSharedBuffer(message, len)); +} +bool AsyncWebSocket::binary(uint32_t id, const char *message, size_t len) { + return binary(id, (const uint8_t *)message, len); +} +bool AsyncWebSocket::binary(uint32_t id, const char *message) { + return binary(id, message, strlen(message)); +} +bool AsyncWebSocket::binary(uint32_t id, const String &message) { + return binary(id, message.c_str(), message.length()); +} + +#ifdef ESP8266 +bool AsyncWebSocket::binary(uint32_t id, const __FlashStringHelper *data, size_t len) { + PGM_P p = reinterpret_cast<PGM_P>(data); + char *message = (char *)malloc(len); + bool enqueued = false; + if (message) { + memcpy_P(message, p, len); + enqueued = binary(id, message, len); + free(message); + } + return enqueued; +} +#endif // ESP8266 + +bool AsyncWebSocket::binary(uint32_t id, AsyncWebSocketMessageBuffer *buffer) { + bool enqueued = false; + if (buffer) { + enqueued = binary(id, std::move(buffer->_buffer)); + delete buffer; + } + return enqueued; +} +bool AsyncWebSocket::binary(uint32_t id, AsyncWebSocketSharedBuffer buffer) { + AsyncWebSocketClient *c = client(id); + return c && c->binary(buffer); +} + +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(const uint8_t *message, size_t len) { + return binaryAll(makeSharedBuffer(message, len)); +} +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(const char *message, size_t len) { + return binaryAll((const uint8_t *)message, len); +} +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(const char *message) { + return binaryAll(message, strlen(message)); +} +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(const String &message) { + return binaryAll(message.c_str(), message.length()); +} + +#ifdef ESP8266 +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(const __FlashStringHelper *data, size_t len) { + PGM_P p = reinterpret_cast<PGM_P>(data); + char *message = (char *)malloc(len); + AsyncWebSocket::SendStatus status = DISCARDED; + if (message) { + memcpy_P(message, p, len); + status = binaryAll(message, len); + free(message); + } + return status; +} +#endif // ESP8266 + +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(AsyncWebSocketMessageBuffer *buffer) { + AsyncWebSocket::SendStatus status = DISCARDED; + if (buffer) { + status = binaryAll(std::move(buffer->_buffer)); + delete buffer; + } + return status; +} +AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(AsyncWebSocketSharedBuffer buffer) { + size_t hit = 0; + size_t miss = 0; + for (auto &c : _clients) { + if (c.status() == WS_CONNECTED && c.binary(buffer)) { + hit++; + } else { + miss++; + } + } + return hit == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED); +} + +size_t AsyncWebSocket::printf(uint32_t id, const char *format, ...) { + AsyncWebSocketClient *c = client(id); + if (c) { + va_list arg; + va_start(arg, format); + size_t len = c->printf(format, arg); + va_end(arg); + return len; + } + return 0; +} + +size_t AsyncWebSocket::printfAll(const char *format, ...) { + va_list arg; + va_start(arg, format); + size_t len = vsnprintf(nullptr, 0, format, arg); + va_end(arg); + + if (len == 0) { + return 0; + } + + char *buffer = new char[len + 1]; + + if (!buffer) { + return 0; + } + + va_start(arg, format); + len = vsnprintf(buffer, len + 1, format, arg); + va_end(arg); + + AsyncWebSocket::SendStatus status = textAll(buffer, len); + delete[] buffer; + return status == DISCARDED ? 0 : len; +} + +#ifdef ESP8266 +size_t AsyncWebSocket::printf_P(uint32_t id, PGM_P formatP, ...) { + AsyncWebSocketClient *c = client(id); + if (c != NULL) { + va_list arg; + va_start(arg, formatP); + size_t len = c->printf_P(formatP, arg); + va_end(arg); + return len; + } + return 0; +} + +size_t AsyncWebSocket::printfAll_P(PGM_P formatP, ...) { + va_list arg; + va_start(arg, formatP); + size_t len = vsnprintf_P(nullptr, 0, formatP, arg); + va_end(arg); + + if (len == 0) { + return 0; + } + + char *buffer = new char[len + 1]; + + if (!buffer) { + return 0; + } + + va_start(arg, formatP); + len = vsnprintf_P(buffer, len + 1, formatP, arg); + va_end(arg); + + AsyncWebSocket::SendStatus status = textAll(buffer, len); + delete[] buffer; + return status == DISCARDED ? 0 : len; +} +#endif + +const char __WS_STR_CONNECTION[] PROGMEM = {"Connection"}; +const char __WS_STR_UPGRADE[] PROGMEM = {"Upgrade"}; +const char __WS_STR_ORIGIN[] PROGMEM = {"Origin"}; +const char __WS_STR_COOKIE[] PROGMEM = {"Cookie"}; +const char __WS_STR_VERSION[] PROGMEM = {"Sec-WebSocket-Version"}; +const char __WS_STR_KEY[] PROGMEM = {"Sec-WebSocket-Key"}; +const char __WS_STR_PROTOCOL[] PROGMEM = {"Sec-WebSocket-Protocol"}; +const char __WS_STR_ACCEPT[] PROGMEM = {"Sec-WebSocket-Accept"}; +const char __WS_STR_UUID[] PROGMEM = {"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"}; + +#define WS_STR_UUID_LEN 36 + +#define WS_STR_CONNECTION FPSTR(__WS_STR_CONNECTION) +#define WS_STR_UPGRADE FPSTR(__WS_STR_UPGRADE) +#define WS_STR_ORIGIN FPSTR(__WS_STR_ORIGIN) +#define WS_STR_COOKIE FPSTR(__WS_STR_COOKIE) +#define WS_STR_VERSION FPSTR(__WS_STR_VERSION) +#define WS_STR_KEY FPSTR(__WS_STR_KEY) +#define WS_STR_PROTOCOL FPSTR(__WS_STR_PROTOCOL) +#define WS_STR_ACCEPT FPSTR(__WS_STR_ACCEPT) +#define WS_STR_UUID FPSTR(__WS_STR_UUID) + +bool AsyncWebSocket::canHandle(AsyncWebServerRequest *request) const { + return _enabled && request->isWebSocketUpgrade() && request->url().equals(_url); +} + +void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request) { + if (!request->hasHeader(WS_STR_VERSION) || !request->hasHeader(WS_STR_KEY)) { + request->send(400); + return; + } + if (_handshakeHandler != nullptr) { + if (!_handshakeHandler(request)) { + request->send(401); + return; + } + } + const AsyncWebHeader *version = request->getHeader(WS_STR_VERSION); + if (version->value().toInt() != 13) { + AsyncWebServerResponse *response = request->beginResponse(400); + response->addHeader(WS_STR_VERSION, T_13); + request->send(response); + return; + } + const AsyncWebHeader *key = request->getHeader(WS_STR_KEY); + AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), this); + if (response == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return; + } + if (request->hasHeader(WS_STR_PROTOCOL)) { + const AsyncWebHeader *protocol = request->getHeader(WS_STR_PROTOCOL); + // ToDo: check protocol + response->addHeader(WS_STR_PROTOCOL, protocol->value()); + } + request->send(response); +} + +AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(size_t size) { + return new AsyncWebSocketMessageBuffer(size); +} + +AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(const uint8_t *data, size_t size) { + return new AsyncWebSocketMessageBuffer(data, size); +} + +/* + * Response to Web Socket request - sends the authorization and detaches the TCP Client from the web server + * Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480 + */ + +AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) { + _server = server; + _code = 101; + _sendContentLength = false; + + uint8_t hash[20]; + char buffer[33]; + +#if defined(ESP8266) || defined(TARGET_RP2040) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(TARGET_RP2350) + sha1(key + WS_STR_UUID, hash); +#else + String k; + if (!k.reserve(key.length() + WS_STR_UUID_LEN)) { + log_e("Failed to allocate"); + return; + } + k.concat(key); + k.concat(WS_STR_UUID); + SHA1Builder sha1; + sha1.begin(); + sha1.add((const uint8_t *)k.c_str(), k.length()); + sha1.calculate(); + sha1.getBytes(hash); +#endif + base64_encodestate _state; + base64_init_encodestate(&_state); + int len = base64_encode_block((const char *)hash, 20, buffer, &_state); + len = base64_encode_blockend((buffer + len), &_state); + addHeader(WS_STR_CONNECTION, WS_STR_UPGRADE); + addHeader(WS_STR_UPGRADE, T_WS); + addHeader(WS_STR_ACCEPT, buffer); +} + +void AsyncWebSocketResponse::_respond(AsyncWebServerRequest *request) { + if (_state == RESPONSE_FAILED) { + request->client()->close(true); + return; + } + String out; + _assembleHead(out, request->version()); + request->client()->write(out.c_str(), _headLength); + _state = RESPONSE_WAIT_ACK; +} + +size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) { + (void)time; + + if (len) { + _server->_newClient(request); + } + + return 0; +} diff --git a/libraries/ESP_Async_WebServer/src/AsyncWebSocket.h b/libraries/ESP_Async_WebServer/src/AsyncWebSocket.h new file mode 100644 index 0000000..122aca9 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/AsyncWebSocket.h @@ -0,0 +1,499 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNCWEBSOCKET_H_ +#define ASYNCWEBSOCKET_H_ + +#include <Arduino.h> +#ifdef ESP32 +#include <AsyncTCP.h> +#include <mutex> +#ifndef WS_MAX_QUEUED_MESSAGES +#define WS_MAX_QUEUED_MESSAGES 32 +#endif +#elif defined(ESP8266) +#include <ESPAsyncTCP.h> +#ifndef WS_MAX_QUEUED_MESSAGES +#define WS_MAX_QUEUED_MESSAGES 8 +#endif +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include <RPAsyncTCP.h> +#ifndef WS_MAX_QUEUED_MESSAGES +#define WS_MAX_QUEUED_MESSAGES 32 +#endif +#endif + +#include <ESPAsyncWebServer.h> + +#include <memory> + +#ifdef ESP8266 +#include <Hash.h> +#ifdef CRYPTO_HASH_h // include Hash.h from espressif framework if the first include was from the crypto library +#include <../src/Hash.h> +#endif +#endif + +#ifndef DEFAULT_MAX_WS_CLIENTS +#ifdef ESP32 +#define DEFAULT_MAX_WS_CLIENTS 8 +#else +#define DEFAULT_MAX_WS_CLIENTS 4 +#endif +#endif + +using AsyncWebSocketSharedBuffer = std::shared_ptr<std::vector<uint8_t>>; + +class AsyncWebSocket; +class AsyncWebSocketResponse; +class AsyncWebSocketClient; +class AsyncWebSocketControl; + +typedef struct { + /** Message type as defined by enum AwsFrameType. + * Note: Applications will only see WS_TEXT and WS_BINARY. + * All other types are handled by the library. */ + uint8_t message_opcode; + /** Frame number of a fragmented message. */ + uint32_t num; + /** Is this the last frame in a fragmented message ?*/ + uint8_t final; + /** Is this frame masked? */ + uint8_t masked; + /** Message type as defined by enum AwsFrameType. + * This value is the same as message_opcode for non-fragmented + * messages, but may also be WS_CONTINUATION in a fragmented message. */ + uint8_t opcode; + /** Length of the current frame. + * This equals the total length of the message if num == 0 && final == true */ + uint64_t len; + /** Mask key */ + uint8_t mask[4]; + /** Offset of the data inside the current frame. */ + uint64_t index; +} AwsFrameInfo; + +typedef enum { + WS_DISCONNECTED, + WS_CONNECTED, + WS_DISCONNECTING +} AwsClientStatus; +typedef enum { + WS_CONTINUATION, + WS_TEXT, + WS_BINARY, + WS_DISCONNECT = 0x08, + WS_PING, + WS_PONG +} AwsFrameType; +typedef enum { + WS_MSG_SENDING, + WS_MSG_SENT, + WS_MSG_ERROR +} AwsMessageStatus; +typedef enum { + WS_EVT_CONNECT, + WS_EVT_DISCONNECT, + WS_EVT_PING, + WS_EVT_PONG, + WS_EVT_ERROR, + WS_EVT_DATA +} AwsEventType; + +class AsyncWebSocketMessageBuffer { + friend AsyncWebSocket; + friend AsyncWebSocketClient; + +private: + AsyncWebSocketSharedBuffer _buffer; + +public: + AsyncWebSocketMessageBuffer() {} + explicit AsyncWebSocketMessageBuffer(size_t size); + AsyncWebSocketMessageBuffer(const uint8_t *data, size_t size); + //~AsyncWebSocketMessageBuffer(); + bool reserve(size_t size); + uint8_t *get() { + return _buffer->data(); + } + size_t length() const { + return _buffer->size(); + } +}; + +class AsyncWebSocketMessage { +private: + AsyncWebSocketSharedBuffer _WSbuffer; + uint8_t _opcode{WS_TEXT}; + bool _mask{false}; + AwsMessageStatus _status{WS_MSG_ERROR}; + size_t _sent{}; + size_t _ack{}; + size_t _acked{}; + +public: + AsyncWebSocketMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false); + + bool finished() const { + return _status != WS_MSG_SENDING; + } + bool betweenFrames() const { + return _acked == _ack; + } + + void ack(size_t len, uint32_t time); + size_t send(AsyncClient *client); +}; + +class AsyncWebSocketClient { +private: + AsyncClient *_client; + AsyncWebSocket *_server; + uint32_t _clientId; + AwsClientStatus _status; +#ifdef ESP32 + mutable std::recursive_mutex _lock; +#endif + std::deque<AsyncWebSocketControl> _controlQueue; + std::deque<AsyncWebSocketMessage> _messageQueue; + bool closeWhenFull = true; + + uint8_t _pstate; + AwsFrameInfo _pinfo; + + uint32_t _lastMessageTime; + uint32_t _keepAlivePeriod; + + bool _queueControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false); + bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false); + void _runQueue(); + void _clearQueue(); + +public: + void *_tempObject; + + AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server); + ~AsyncWebSocketClient(); + + // client id increments for the given server + uint32_t id() const { + return _clientId; + } + AwsClientStatus status() const { + return _status; + } + AsyncClient *client() { + return _client; + } + const AsyncClient *client() const { + return _client; + } + AsyncWebSocket *server() { + return _server; + } + const AsyncWebSocket *server() const { + return _server; + } + AwsFrameInfo const &pinfo() const { + return _pinfo; + } + + // - If "true" (default), the connection will be closed if the message queue is full. + // This is the default behavior in yubox-node-org, which is not silently discarding messages but instead closes the connection. + // The big issue with this behavior is that is can cause the UI to automatically re-create a new WS connection, which can be filled again, + // and so on, causing a resource exhaustion. + // + // - If "false", the incoming message will be discarded if the queue is full. + // This is the default behavior in the original ESPAsyncWebServer library from me-no-dev. + // This behavior allows the best performance at the expense of unreliable message delivery in case the queue is full (some messages may be lost). + // + // - In any case, when the queue is full, a message is logged. + // - IT is recommended to use the methods queueIsFull(), availableForWriteAll(), availableForWrite(clientId) to check if the queue is full before sending a message. + // + // Usage: + // - can be set in the onEvent listener when connecting (event type is: WS_EVT_CONNECT) + // + // Use cases:, + // - if using websocket to send logging messages, maybe some loss is acceptable. + // - But if using websocket to send UI update messages, maybe the connection should be closed and the UI redrawn. + void setCloseClientOnQueueFull(bool close) { + closeWhenFull = close; + } + bool willCloseClientOnQueueFull() const { + return closeWhenFull; + } + + IPAddress remoteIP() const; + uint16_t remotePort() const; + + bool shouldBeDeleted() const { + return !_client; + } + + // control frames + void close(uint16_t code = 0, const char *message = NULL); + bool ping(const uint8_t *data = NULL, size_t len = 0); + + // set auto-ping period in seconds. disabled if zero (default) + void keepAlivePeriod(uint16_t seconds) { + _keepAlivePeriod = seconds * 1000; + } + uint16_t keepAlivePeriod() { + return (uint16_t)(_keepAlivePeriod / 1000); + } + + // data packets + void message(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false) { + _queueMessage(buffer, opcode, mask); + } + bool queueIsFull() const; + size_t queueLen() const; + + size_t printf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + bool text(AsyncWebSocketSharedBuffer buffer); + bool text(const uint8_t *message, size_t len); + bool text(const char *message, size_t len); + bool text(const char *message); + bool text(const String &message); + bool text(AsyncWebSocketMessageBuffer *buffer); + + bool binary(AsyncWebSocketSharedBuffer buffer); + bool binary(const uint8_t *message, size_t len); + bool binary(const char *message, size_t len); + bool binary(const char *message); + bool binary(const String &message); + bool binary(AsyncWebSocketMessageBuffer *buffer); + + bool canSend() const; + + // system callbacks (do not call) + void _onAck(size_t len, uint32_t time); + void _onError(int8_t); + void _onPoll(); + void _onTimeout(uint32_t time); + void _onDisconnect(); + void _onData(void *pbuf, size_t plen); + +#ifdef ESP8266 + size_t printf_P(PGM_P formatP, ...) __attribute__((format(printf, 2, 3))); + bool text(const __FlashStringHelper *message); + bool binary(const __FlashStringHelper *message, size_t len); +#endif +}; + +using AwsHandshakeHandler = std::function<bool(AsyncWebServerRequest *request)>; +using AwsEventHandler = std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)>; + +// WebServer Handler implementation that plays the role of a socket server +class AsyncWebSocket : public AsyncWebHandler { +private: + String _url; + std::list<AsyncWebSocketClient> _clients; + uint32_t _cNextId; + AwsEventHandler _eventHandler; + AwsHandshakeHandler _handshakeHandler; + bool _enabled; +#ifdef ESP32 + mutable std::mutex _lock; +#endif + +public: + typedef enum { + DISCARDED = 0, + ENQUEUED = 1, + PARTIALLY_ENQUEUED = 2, + } SendStatus; + + explicit AsyncWebSocket(const char *url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {} + AsyncWebSocket(const String &url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {} + ~AsyncWebSocket(){}; + const char *url() const { + return _url.c_str(); + } + void enable(bool e) { + _enabled = e; + } + bool enabled() const { + return _enabled; + } + bool availableForWriteAll(); + bool availableForWrite(uint32_t id); + + size_t count() const; + AsyncWebSocketClient *client(uint32_t id); + bool hasClient(uint32_t id) { + return client(id) != nullptr; + } + + void close(uint32_t id, uint16_t code = 0, const char *message = NULL); + void closeAll(uint16_t code = 0, const char *message = NULL); + void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS); + + bool ping(uint32_t id, const uint8_t *data = NULL, size_t len = 0); + SendStatus pingAll(const uint8_t *data = NULL, size_t len = 0); // done + + bool text(uint32_t id, const uint8_t *message, size_t len); + bool text(uint32_t id, const char *message, size_t len); + bool text(uint32_t id, const char *message); + bool text(uint32_t id, const String &message); + bool text(uint32_t id, AsyncWebSocketMessageBuffer *buffer); + bool text(uint32_t id, AsyncWebSocketSharedBuffer buffer); + + SendStatus textAll(const uint8_t *message, size_t len); + SendStatus textAll(const char *message, size_t len); + SendStatus textAll(const char *message); + SendStatus textAll(const String &message); + SendStatus textAll(AsyncWebSocketMessageBuffer *buffer); + SendStatus textAll(AsyncWebSocketSharedBuffer buffer); + + bool binary(uint32_t id, const uint8_t *message, size_t len); + bool binary(uint32_t id, const char *message, size_t len); + bool binary(uint32_t id, const char *message); + bool binary(uint32_t id, const String &message); + bool binary(uint32_t id, AsyncWebSocketMessageBuffer *buffer); + bool binary(uint32_t id, AsyncWebSocketSharedBuffer buffer); + + SendStatus binaryAll(const uint8_t *message, size_t len); + SendStatus binaryAll(const char *message, size_t len); + SendStatus binaryAll(const char *message); + SendStatus binaryAll(const String &message); + SendStatus binaryAll(AsyncWebSocketMessageBuffer *buffer); + SendStatus binaryAll(AsyncWebSocketSharedBuffer buffer); + + size_t printf(uint32_t id, const char *format, ...) __attribute__((format(printf, 3, 4))); + size_t printfAll(const char *format, ...) __attribute__((format(printf, 2, 3))); + +#ifdef ESP8266 + bool text(uint32_t id, const __FlashStringHelper *message); + SendStatus textAll(const __FlashStringHelper *message); + bool binary(uint32_t id, const __FlashStringHelper *message, size_t len); + SendStatus binaryAll(const __FlashStringHelper *message, size_t len); + size_t printf_P(uint32_t id, PGM_P formatP, ...) __attribute__((format(printf, 3, 4))); + size_t printfAll_P(PGM_P formatP, ...) __attribute__((format(printf, 2, 3))); +#endif + + void onEvent(AwsEventHandler handler) { + _eventHandler = handler; + } + void handleHandshake(AwsHandshakeHandler handler) { + _handshakeHandler = handler; + } + + // system callbacks (do not call) + uint32_t _getNextId() { + return _cNextId++; + } + AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); + void _handleDisconnect(AsyncWebSocketClient *client); + void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; + + // messagebuffer functions/objects. + AsyncWebSocketMessageBuffer *makeBuffer(size_t size = 0); + AsyncWebSocketMessageBuffer *makeBuffer(const uint8_t *data, size_t size); + + std::list<AsyncWebSocketClient> &getClients() { + return _clients; + } +}; + +// WebServer response to authenticate the socket and detach the tcp client from the web server request +class AsyncWebSocketResponse : public AsyncWebServerResponse { +private: + String _content; + AsyncWebSocket *_server; + +public: + AsyncWebSocketResponse(const String &key, AsyncWebSocket *server); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid() const { + return true; + } +}; + +class AsyncWebSocketMessageHandler { +public: + AwsEventHandler eventHandler() const { + return _handler; + } + + void onConnect(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> onConnect) { + _onConnect = onConnect; + } + + void onDisconnect(std::function<void(AsyncWebSocket *server, uint32_t clientId)> onDisconnect) { + _onDisconnect = onDisconnect; + } + + /** + * Error callback + * @param reason null-terminated string + * @param len length of the string + */ + void onError(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> onError) { + _onError = onError; + } + + /** + * Complete message callback + * @param data pointer to the data (binary or null-terminated string). This handler expects the user to know which data type he uses. + */ + void onMessage(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> onMessage) { + _onMessage = onMessage; + } + + /** + * Fragmented message callback + * @param data pointer to the data (binary or null-terminated string), will be null-terminated. This handler expects the user to know which data type he uses. + */ + // clang-format off + void onFragment(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> onFragment) { + _onFragment = onFragment; + } + // clang-format on + +private: + // clang-format off + std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> _onConnect; + std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> _onError; + std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> _onMessage; + std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> _onFragment; + std::function<void(AsyncWebSocket *server, uint32_t clientId)> _onDisconnect; + // clang-format on + + // this handler is meant to only support 1-frame messages (== unfragmented messages) + AwsEventHandler _handler = [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + if (_onConnect) { + _onConnect(server, client); + } + } else if (type == WS_EVT_DISCONNECT) { + if (_onDisconnect) { + _onDisconnect(server, client->id()); + } + } else if (type == WS_EVT_ERROR) { + if (_onError) { + _onError(server, client, *((uint16_t *)arg), (const char *)data, len); + } + } else if (type == WS_EVT_DATA) { + AwsFrameInfo *info = (AwsFrameInfo *)arg; + if (info->opcode == WS_TEXT) { + data[len] = 0; + } + if (info->final && info->index == 0 && info->len == len) { + if (_onMessage) { + _onMessage(server, client, data, len); + } + } else { + if (_onFragment) { + _onFragment(server, client, info, data, len); + } + } + } + }; +}; + +#endif /* ASYNCWEBSOCKET_H_ */ diff --git a/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.cpp b/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.cpp new file mode 100644 index 0000000..06a73a5 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.cpp @@ -0,0 +1,284 @@ +/* + * FIPS-180-1 compliant SHA-1 implementation + * + * Copyright (C) 2006-2015, ARM Limited, All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is part of mbed TLS (https://tls.mbed.org) + * Modified for esp32 by Lucas Saavedra Vaz on 11 Jan 2024 + */ + +#include <Arduino.h> +#if ESP_IDF_VERSION_MAJOR < 5 + +#include "BackPort_SHA1Builder.h" + +// 32-bit integer manipulation macros (big endian) + +#ifndef GET_UINT32_BE +#define GET_UINT32_BE(n, b, i) \ + { (n) = ((uint32_t)(b)[(i)] << 24) | ((uint32_t)(b)[(i) + 1] << 16) | ((uint32_t)(b)[(i) + 2] << 8) | ((uint32_t)(b)[(i) + 3]); } +#endif + +#ifndef PUT_UINT32_BE +#define PUT_UINT32_BE(n, b, i) \ + { \ + (b)[(i)] = (uint8_t)((n) >> 24); \ + (b)[(i) + 1] = (uint8_t)((n) >> 16); \ + (b)[(i) + 2] = (uint8_t)((n) >> 8); \ + (b)[(i) + 3] = (uint8_t)((n)); \ + } +#endif + +// Constants + +static const uint8_t sha1_padding[64] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +// Private methods + +void SHA1Builder::process(const uint8_t *data) { + uint32_t temp, W[16], A, B, C, D, E; + + GET_UINT32_BE(W[0], data, 0); + GET_UINT32_BE(W[1], data, 4); + GET_UINT32_BE(W[2], data, 8); + GET_UINT32_BE(W[3], data, 12); + GET_UINT32_BE(W[4], data, 16); + GET_UINT32_BE(W[5], data, 20); + GET_UINT32_BE(W[6], data, 24); + GET_UINT32_BE(W[7], data, 28); + GET_UINT32_BE(W[8], data, 32); + GET_UINT32_BE(W[9], data, 36); + GET_UINT32_BE(W[10], data, 40); + GET_UINT32_BE(W[11], data, 44); + GET_UINT32_BE(W[12], data, 48); + GET_UINT32_BE(W[13], data, 52); + GET_UINT32_BE(W[14], data, 56); + GET_UINT32_BE(W[15], data, 60); + +#define sha1_S(x, n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n))) + +#define sha1_R(t) (temp = W[(t - 3) & 0x0F] ^ W[(t - 8) & 0x0F] ^ W[(t - 14) & 0x0F] ^ W[t & 0x0F], (W[t & 0x0F] = sha1_S(temp, 1))) + +#define sha1_P(a, b, c, d, e, x) \ + { \ + e += sha1_S(a, 5) + sha1_F(b, c, d) + sha1_K + x; \ + b = sha1_S(b, 30); \ + } + + A = state[0]; + B = state[1]; + C = state[2]; + D = state[3]; + E = state[4]; + +#define sha1_F(x, y, z) (z ^ (x & (y ^ z))) +#define sha1_K 0x5A827999 + + sha1_P(A, B, C, D, E, W[0]); + sha1_P(E, A, B, C, D, W[1]); + sha1_P(D, E, A, B, C, W[2]); + sha1_P(C, D, E, A, B, W[3]); + sha1_P(B, C, D, E, A, W[4]); + sha1_P(A, B, C, D, E, W[5]); + sha1_P(E, A, B, C, D, W[6]); + sha1_P(D, E, A, B, C, W[7]); + sha1_P(C, D, E, A, B, W[8]); + sha1_P(B, C, D, E, A, W[9]); + sha1_P(A, B, C, D, E, W[10]); + sha1_P(E, A, B, C, D, W[11]); + sha1_P(D, E, A, B, C, W[12]); + sha1_P(C, D, E, A, B, W[13]); + sha1_P(B, C, D, E, A, W[14]); + sha1_P(A, B, C, D, E, W[15]); + sha1_P(E, A, B, C, D, sha1_R(16)); + sha1_P(D, E, A, B, C, sha1_R(17)); + sha1_P(C, D, E, A, B, sha1_R(18)); + sha1_P(B, C, D, E, A, sha1_R(19)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x, y, z) (x ^ y ^ z) +#define sha1_K 0x6ED9EBA1 + + sha1_P(A, B, C, D, E, sha1_R(20)); + sha1_P(E, A, B, C, D, sha1_R(21)); + sha1_P(D, E, A, B, C, sha1_R(22)); + sha1_P(C, D, E, A, B, sha1_R(23)); + sha1_P(B, C, D, E, A, sha1_R(24)); + sha1_P(A, B, C, D, E, sha1_R(25)); + sha1_P(E, A, B, C, D, sha1_R(26)); + sha1_P(D, E, A, B, C, sha1_R(27)); + sha1_P(C, D, E, A, B, sha1_R(28)); + sha1_P(B, C, D, E, A, sha1_R(29)); + sha1_P(A, B, C, D, E, sha1_R(30)); + sha1_P(E, A, B, C, D, sha1_R(31)); + sha1_P(D, E, A, B, C, sha1_R(32)); + sha1_P(C, D, E, A, B, sha1_R(33)); + sha1_P(B, C, D, E, A, sha1_R(34)); + sha1_P(A, B, C, D, E, sha1_R(35)); + sha1_P(E, A, B, C, D, sha1_R(36)); + sha1_P(D, E, A, B, C, sha1_R(37)); + sha1_P(C, D, E, A, B, sha1_R(38)); + sha1_P(B, C, D, E, A, sha1_R(39)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x, y, z) ((x & y) | (z & (x | y))) +#define sha1_K 0x8F1BBCDC + + sha1_P(A, B, C, D, E, sha1_R(40)); + sha1_P(E, A, B, C, D, sha1_R(41)); + sha1_P(D, E, A, B, C, sha1_R(42)); + sha1_P(C, D, E, A, B, sha1_R(43)); + sha1_P(B, C, D, E, A, sha1_R(44)); + sha1_P(A, B, C, D, E, sha1_R(45)); + sha1_P(E, A, B, C, D, sha1_R(46)); + sha1_P(D, E, A, B, C, sha1_R(47)); + sha1_P(C, D, E, A, B, sha1_R(48)); + sha1_P(B, C, D, E, A, sha1_R(49)); + sha1_P(A, B, C, D, E, sha1_R(50)); + sha1_P(E, A, B, C, D, sha1_R(51)); + sha1_P(D, E, A, B, C, sha1_R(52)); + sha1_P(C, D, E, A, B, sha1_R(53)); + sha1_P(B, C, D, E, A, sha1_R(54)); + sha1_P(A, B, C, D, E, sha1_R(55)); + sha1_P(E, A, B, C, D, sha1_R(56)); + sha1_P(D, E, A, B, C, sha1_R(57)); + sha1_P(C, D, E, A, B, sha1_R(58)); + sha1_P(B, C, D, E, A, sha1_R(59)); + +#undef sha1_K +#undef sha1_F + +#define sha1_F(x, y, z) (x ^ y ^ z) +#define sha1_K 0xCA62C1D6 + + sha1_P(A, B, C, D, E, sha1_R(60)); + sha1_P(E, A, B, C, D, sha1_R(61)); + sha1_P(D, E, A, B, C, sha1_R(62)); + sha1_P(C, D, E, A, B, sha1_R(63)); + sha1_P(B, C, D, E, A, sha1_R(64)); + sha1_P(A, B, C, D, E, sha1_R(65)); + sha1_P(E, A, B, C, D, sha1_R(66)); + sha1_P(D, E, A, B, C, sha1_R(67)); + sha1_P(C, D, E, A, B, sha1_R(68)); + sha1_P(B, C, D, E, A, sha1_R(69)); + sha1_P(A, B, C, D, E, sha1_R(70)); + sha1_P(E, A, B, C, D, sha1_R(71)); + sha1_P(D, E, A, B, C, sha1_R(72)); + sha1_P(C, D, E, A, B, sha1_R(73)); + sha1_P(B, C, D, E, A, sha1_R(74)); + sha1_P(A, B, C, D, E, sha1_R(75)); + sha1_P(E, A, B, C, D, sha1_R(76)); + sha1_P(D, E, A, B, C, sha1_R(77)); + sha1_P(C, D, E, A, B, sha1_R(78)); + sha1_P(B, C, D, E, A, sha1_R(79)); + +#undef sha1_K +#undef sha1_F + + state[0] += A; + state[1] += B; + state[2] += C; + state[3] += D; + state[4] += E; +} + +// Public methods + +void SHA1Builder::begin() { + total[0] = 0; + total[1] = 0; + + state[0] = 0x67452301; + state[1] = 0xEFCDAB89; + state[2] = 0x98BADCFE; + state[3] = 0x10325476; + state[4] = 0xC3D2E1F0; + + memset(buffer, 0x00, sizeof(buffer)); + memset(hash, 0x00, sizeof(hash)); +} + +void SHA1Builder::add(const uint8_t *data, size_t len) { + size_t fill; + uint32_t left; + + if (len == 0) { + return; + } + + left = total[0] & 0x3F; + fill = 64 - left; + + total[0] += (uint32_t)len; + total[0] &= 0xFFFFFFFF; + + if (total[0] < (uint32_t)len) { + total[1]++; + } + + if (left && len >= fill) { + memcpy((void *)(buffer + left), data, fill); + process(buffer); + data += fill; + len -= fill; + left = 0; + } + + while (len >= 64) { + process(data); + data += 64; + len -= 64; + } + + if (len > 0) { + memcpy((void *)(buffer + left), data, len); + } +} + +void SHA1Builder::calculate(void) { + uint32_t last, padn; + uint32_t high, low; + uint8_t msglen[8]; + + high = (total[0] >> 29) | (total[1] << 3); + low = (total[0] << 3); + + PUT_UINT32_BE(high, msglen, 0); + PUT_UINT32_BE(low, msglen, 4); + + last = total[0] & 0x3F; + padn = (last < 56) ? (56 - last) : (120 - last); + + add((uint8_t *)sha1_padding, padn); + add(msglen, 8); + + PUT_UINT32_BE(state[0], hash, 0); + PUT_UINT32_BE(state[1], hash, 4); + PUT_UINT32_BE(state[2], hash, 8); + PUT_UINT32_BE(state[3], hash, 12); + PUT_UINT32_BE(state[4], hash, 16); +} + +void SHA1Builder::getBytes(uint8_t *output) { + memcpy(output, hash, SHA1_HASH_SIZE); +} + +#endif // ESP_IDF_VERSION_MAJOR < 5 diff --git a/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.h b/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.h new file mode 100644 index 0000000..e7eafbe --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/BackPort_SHA1Builder.h @@ -0,0 +1,44 @@ +// Copyright 2024 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <Arduino.h> +#if ESP_IDF_VERSION_MAJOR < 5 + +#ifndef SHA1Builder_h +#define SHA1Builder_h + +#include <Stream.h> +#include <WString.h> + +#define SHA1_HASH_SIZE 20 + +class SHA1Builder { +private: + uint32_t total[2]; /* number of bytes processed */ + uint32_t state[5]; /* intermediate digest state */ + unsigned char buffer[64]; /* data block being processed */ + uint8_t hash[SHA1_HASH_SIZE]; /* SHA-1 result */ + + void process(const uint8_t *data); + +public: + void begin(); + void add(const uint8_t *data, size_t len); + void calculate(); + void getBytes(uint8_t *output); +}; + +#endif // SHA1Builder_h + +#endif // ESP_IDF_VERSION_MAJOR < 5 diff --git a/libraries/ESP_Async_WebServer/src/ChunkPrint.cpp b/libraries/ESP_Async_WebServer/src/ChunkPrint.cpp new file mode 100644 index 0000000..4617d34 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/ChunkPrint.cpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include <ChunkPrint.h> + +ChunkPrint::ChunkPrint(uint8_t *destination, size_t from, size_t len) : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {} + +size_t ChunkPrint::write(uint8_t c) { + if (_to_skip > 0) { + _to_skip--; + return 1; + } else if (_to_write > 0) { + _to_write--; + _destination[_pos++] = c; + return 1; + } + return 0; +} diff --git a/libraries/ESP_Async_WebServer/src/ChunkPrint.h b/libraries/ESP_Async_WebServer/src/ChunkPrint.h new file mode 100644 index 0000000..04938b3 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/ChunkPrint.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef CHUNKPRINT_H +#define CHUNKPRINT_H + +#include <Print.h> + +class ChunkPrint : public Print { +private: + uint8_t *_destination; + size_t _to_skip; + size_t _to_write; + size_t _pos; + +public: + ChunkPrint(uint8_t *destination, size_t from, size_t len); + 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/libraries/ESP_Async_WebServer/src/ESPAsyncWebServer.h b/libraries/ESP_Async_WebServer/src/ESPAsyncWebServer.h new file mode 100644 index 0000000..24233cd --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/ESPAsyncWebServer.h @@ -0,0 +1,1217 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef _ESPAsyncWebServer_H_ +#define _ESPAsyncWebServer_H_ + +#include <Arduino.h> +#include <FS.h> +#include <lwip/tcpbase.h> + +#include <algorithm> +#include <deque> +#include <functional> +#include <list> +#include <unordered_map> +#include <vector> + +#ifdef ESP32 +#include <AsyncTCP.h> +#include <WiFi.h> +#elif defined(ESP8266) +#include <ESP8266WiFi.h> +#include <ESPAsyncTCP.h> +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include <RPAsyncTCP.h> +#include <HTTP_Method.h> +#include <WiFi.h> +#include <http_parser.h> +#else +#error Platform not supported +#endif + +#include "literals.h" + +#include "AsyncWebServerVersion.h" +#define ASYNCWEBSERVER_FORK_ESP32Async + +#ifdef ASYNCWEBSERVER_REGEX +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE +#else +#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined"))) +#endif + +// See https://github.com/ESP32Async/ESPAsyncWebServer/commit/3d3456e9e81502a477f6498c44d0691499dda8f9#diff-646b25b11691c11dce25529e3abce843f0ba4bd07ab75ec9eee7e72b06dbf13fR388-R392 +// This setting slowdown chunk serving but avoids crashing or deadlocks in the case where slow chunk responses are created, like file serving form SD Card +#ifndef ASYNCWEBSERVER_USE_CHUNK_INFLIGHT +#define ASYNCWEBSERVER_USE_CHUNK_INFLIGHT 1 +#endif + +class AsyncWebServer; +class AsyncWebServerRequest; +class AsyncWebServerResponse; +class AsyncWebHeader; +class AsyncWebParameter; +class AsyncWebRewrite; +class AsyncWebHandler; +class AsyncStaticWebHandler; +class AsyncCallbackWebHandler; +class AsyncResponseStream; +class AsyncMiddlewareChain; + +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +typedef enum http_method WebRequestMethod; +#else +#ifndef WEBSERVER_H +typedef enum { + HTTP_GET = 0b00000001, + HTTP_POST = 0b00000010, + HTTP_DELETE = 0b00000100, + HTTP_PUT = 0b00001000, + HTTP_PATCH = 0b00010000, + HTTP_HEAD = 0b00100000, + HTTP_OPTIONS = 0b01000000, + HTTP_ANY = 0b01111111, +} WebRequestMethod; +#endif +#endif + +#ifndef HAVE_FS_FILE_OPEN_MODE +namespace fs { +class FileOpenMode { +public: + static const char *read; + static const char *write; + static const char *append; +}; +}; // namespace fs +#else +#include "FileOpenMode.h" +#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_STREAM_BUFFER_SIZE 1460 + +typedef uint8_t WebRequestMethodComposite; +typedef std::function<void(void)> ArDisconnectHandler; + +/* + * PARAMETER :: Chainable object to hold GET/POST and FILE parameters + * */ + +class AsyncWebParameter { +private: + String _name; + String _value; + size_t _size; + bool _isForm; + bool _isFile; + +public: + AsyncWebParameter(const String &name, const String &value, bool form = false, bool file = false, size_t size = 0) + : _name(name), _value(value), _size(size), _isForm(form), _isFile(file) {} + const String &name() const { + return _name; + } + const String &value() const { + return _value; + } + size_t size() const { + return _size; + } + bool isPost() const { + return _isForm; + } + bool isFile() const { + return _isFile; + } +}; + +/* + * HEADER :: Chainable object to hold the headers + * */ + +class AsyncWebHeader { +private: + String _name; + String _value; + +public: + 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); + + AsyncWebHeader &operator=(const AsyncWebHeader &) = default; + + const String &name() const { + return _name; + } + const String &value() const { + return _value; + } + String toString() const; +}; + +/* + * REQUEST :: Each incoming Client is wrapped inside a Request and both live together until disconnect + * */ + +typedef enum { + RCT_NOT_USED = -1, + RCT_DEFAULT = 0, + RCT_HTTP, + RCT_WS, + RCT_EVENT, + RCT_MAX +} RequestedConnectionType; + +// this enum is similar to Arduino WebServer's AsyncAuthType and PsychicHttp +typedef enum { + AUTH_NONE = 0, // always allow + AUTH_BASIC = 1, + AUTH_DIGEST = 2, + AUTH_BEARER = 3, + AUTH_OTHER = 4, + AUTH_DENIED = 255, // always returns 401 +} AsyncAuthType; + +typedef std::function<size_t(uint8_t *, size_t, size_t)> AwsResponseFiller; +typedef std::function<String(const String &)> AwsTemplateProcessor; + +using AsyncWebServerRequestPtr = std::weak_ptr<AsyncWebServerRequest>; + +class AsyncWebServerRequest { + using File = fs::File; + using FS = fs::FS; + friend class AsyncWebServer; + friend class AsyncCallbackWebHandler; + +private: + AsyncClient *_client; + AsyncWebServer *_server; + AsyncWebHandler *_handler; + AsyncWebServerResponse *_response; + ArDisconnectHandler _onDisconnectfn; + + bool _sent = false; // response is sent + bool _paused = false; // request is paused (request continuation) + std::shared_ptr<AsyncWebServerRequest> _this; // shared pointer to this request + + String _temp; + uint8_t _parseState; + + uint8_t _version; + WebRequestMethodComposite _method; + String _url; + String _host; + String _contentType; + String _boundary; + String _authorization; + RequestedConnectionType _reqconntype; + AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE; + bool _isMultipart; + bool _isPlainPost; + bool _expectingContinue; + size_t _contentLength; + size_t _parsedLength; + + std::list<AsyncWebHeader> _headers; + std::list<AsyncWebParameter> _params; + std::list<String> _pathParams; + + std::unordered_map<const char *, String, std::hash<const char *>, std::equal_to<const char *>> _attributes; + + uint8_t _multiParseState; + uint8_t _boundaryPosition; + size_t _itemStartIndex; + size_t _itemSize; + String _itemName; + String _itemFilename; + String _itemType; + String _itemValue; + uint8_t *_itemBuffer; + size_t _itemBufferIndex; + bool _itemIsFile; + + void _onPoll(); + void _onAck(size_t len, uint32_t time); + void _onError(int8_t error); + void _onTimeout(uint32_t time); + void _onDisconnect(); + void _onData(void *buf, size_t len); + + void _addPathParam(const char *param); + + bool _parseReqHead(); + bool _parseReqHeader(); + void _parseLine(); + void _parsePlainPostChar(uint8_t data); + void _parseMultipartPostByte(uint8_t data, bool last); + void _addGetParams(const String ¶ms); + + void _handleUploadStart(); + void _handleUploadByte(uint8_t data, bool last); + void _handleUploadEnd(); + + void _send(); + void _runMiddlewareChain(); + +public: + File _tempFile; + void *_tempObject; + + AsyncWebServerRequest(AsyncWebServer *, AsyncClient *); + ~AsyncWebServerRequest(); + + AsyncClient *client() { + return _client; + } + uint8_t version() const { + return _version; + } + WebRequestMethodComposite method() const { + return _method; + } + const String &url() const { + return _url; + } + const String &host() const { + return _host; + } + const String &contentType() const { + return _contentType; + } + size_t contentLength() const { + return _contentLength; + } + bool multipart() const { + return _isMultipart; + } + + const char *methodToString() const; + const char *requestedConnTypeToString() const; + + RequestedConnectionType requestedConnType() const { + return _reqconntype; + } + bool isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2 = RCT_NOT_USED, RequestedConnectionType erct3 = RCT_NOT_USED) + const; + bool isWebSocketUpgrade() const { + return _method == HTTP_GET && isExpectedRequestedConnType(RCT_WS); + } + bool isSSE() const { + return _method == HTTP_GET && isExpectedRequestedConnType(RCT_EVENT); + } + bool isHTTP() const { + return isExpectedRequestedConnType(RCT_DEFAULT, RCT_HTTP); + } + void onDisconnect(ArDisconnectHandler fn); + + // hash is the string representation of: + // base64(user:pass) for basic or + // user:realm:md5(user:realm:pass) for digest + bool authenticate(const char *hash) const; + bool authenticate(const char *username, const char *credentials, const char *realm = NULL, bool isHash = false) const; + 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); + + // IMPORTANT: this method is for internal use ONLY + // Please do not use it! + // It can be removed or modified at any time without notice + void setHandler(AsyncWebHandler *handler) { + _handler = handler; + } + +#ifndef ESP8266 + [[deprecated("All headers are now collected. Use removeHeader(name) or AsyncHeaderFreeMiddleware 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 AsyncHeaderFreeMiddleware if you really need to free some headers.")]] +#endif + void addInterestingHeader(__unused const String &name) { + } + + /** + * @brief issue HTTP redirect response with Location header + * + * @param url - url to redirect to + * @param code - response code, default is 302 : temporary redirect + */ + 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 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)); + } + + void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { + if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) { + send(beginResponse(fs, path, contentType, download, callback)); + } else { + send(404); + } + } + void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { + send(fs, path, contentType.c_str(), download, callback); + } + + void send(File content, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { + if (content) { + send(beginResponse(content, path, contentType, download, callback)); + } else { + send(404); + } + } + void send(File content, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { + send(content, path, contentType.c_str(), download, callback); + } + + void send(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback = nullptr) { + send(beginResponse(stream, contentType, len, callback)); + } + void send(Stream &stream, const String &contentType, size_t len, AwsTemplateProcessor callback = nullptr) { + send(beginResponse(stream, contentType, len, callback)); + } + + void send(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + send(beginResponse(contentType, len, callback, templateCallback)); + } + void send(const String &contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + send(beginResponse(contentType, len, callback, templateCallback)); + } + + void sendChunked(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + send(beginChunkedResponse(contentType, callback, templateCallback)); + } + void sendChunked(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + send(beginChunkedResponse(contentType, callback, templateCallback)); + } + +#ifndef ESP8266 + [[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(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); + } +#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 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); + } + + AsyncWebServerResponse * + beginResponse(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); + AsyncWebServerResponse * + beginResponse(FS &fs, const String &path, const String &contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { + return beginResponse(fs, path, contentType.c_str(), download, callback); + } + + AsyncWebServerResponse * + beginResponse(File content, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); + AsyncWebServerResponse * + beginResponse(File content, const String &path, const String &contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { + return beginResponse(content, path, contentType.c_str(), download, callback); + } + + AsyncWebServerResponse *beginResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback = nullptr); + AsyncWebServerResponse *beginResponse(Stream &stream, const String &contentType, size_t len, AwsTemplateProcessor callback = nullptr) { + return beginResponse(stream, contentType.c_str(), len, callback); + } + + AsyncWebServerResponse *beginResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); + AsyncWebServerResponse *beginResponse(const String &contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + return beginResponse(contentType.c_str(), len, callback, templateCallback); + } + + AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); + AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) { + return beginChunkedResponse(contentType.c_str(), callback, templateCallback); + } + + 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(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(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 Request Continuation: this function pauses the current request and returns a weak pointer (AsyncWebServerRequestPtr is a std::weak_ptr) to the request in order to reuse it later on. + * The middelware chain will continue to be processed until the end, but no response will be sent. + * To resume operations (send the request), the request must be retrieved from the weak pointer and a send() function must be called. + * AsyncWebServerRequestPtr is the only object allowed to exist the scope of the request handler. + * @warning This function should be called from within the context of a request (in a handler or middleware for example). + * @warning While the request is paused, if the client aborts the request, the latter will be disconnected and deleted. + * So it is the responsibility of the user to check the validity of the request pointer (AsyncWebServerRequestPtr) before using it by calling lock() and/or expired(). + */ + AsyncWebServerRequestPtr pause(); + + bool isPaused() const { + return _paused; + } + + /** + * @brief Aborts the request and close the client (RST). + * Mark the request as sent. + * If it was paused, it will be unpaused and it won't be possible to resume it. + */ + void abort(); + + bool isSent() const { + return _sent; + } + + /** + * @brief Get the Request parameter by name + * + * @param name + * @param post + * @param file + * @return const AsyncWebParameter* + */ + const AsyncWebParameter *getParam(const char *name, bool post = false, bool file = false) const; + + const AsyncWebParameter *getParam(const String &name, bool post = false, bool file = false) const { + return getParam(name.c_str(), post, file); + }; +#ifdef ESP8266 + const AsyncWebParameter *getParam(const __FlashStringHelper *data, bool post, bool file) const; +#endif + + /** + * @brief Get request parameter by number + * i.e., n-th parameter + * @param num + * @return const AsyncWebParameter* + */ + const AsyncWebParameter *getParam(size_t num) const; + const AsyncWebParameter *getParam(int num) const { + return num < 0 ? nullptr : getParam((size_t)num); + } + + size_t args() const { + return params(); + } // get arguments count + + // get request argument value by name + const String &arg(const char *name) const; + // get request argument value by name + const String &arg(const String &name) const { + return arg(name.c_str()); + }; +#ifdef ESP8266 + const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name) +#endif + const String &arg(size_t i) const; // get request argument value by number + const String &arg(int i) const { + return i < 0 ? emptyString : arg((size_t)i); + }; + const String &argName(size_t i) const; // get request argument name by number + const String &argName(int i) const { + return i < 0 ? emptyString : argName((size_t)i); + }; + bool hasArg(const char *name) const; // check if argument exists + bool hasArg(const String &name) const { + return hasArg(name.c_str()); + }; +#ifdef ESP8266 + bool hasArg(const __FlashStringHelper *data) const; // check if F(argument) exists +#endif + + const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; + const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const { + return i < 0 ? emptyString : pathArg((size_t)i); + } + + // get request header value by name + const String &header(const char *name) const; + const String &header(const String &name) const { + return header(name.c_str()); + }; + +#ifdef ESP8266 + const String &header(const __FlashStringHelper *data) const; // get request header value by F(name) +#endif + + const String &header(size_t i) const; // get request header value by number + const String &header(int i) const { + return i < 0 ? emptyString : header((size_t)i); + }; + const String &headerName(size_t i) const; // get request header name by number + const String &headerName(int i) const { + return i < 0 ? emptyString : headerName((size_t)i); + }; + + 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 AsyncWebHeader *getHeader(int num) const { + return num < 0 ? nullptr : getHeader((size_t)num); + }; + + const std::list<AsyncWebHeader> &getHeaders() const { + return _headers; + } + + size_t getHeaderNames(std::vector<const char *> &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; +}; + +/* + * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) + * */ + +using ArRequestFilterFunction = std::function<bool(AsyncWebServerRequest *request)>; + +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<void(void)>; +using ArMiddlewareCallback = std::function<void(AsyncWebServerRequest *request, ArMiddlewareNext next)>; + +// 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: + ~AsyncMiddlewareChain(); + + void addMiddleware(ArMiddlewareCallback fn); + void addMiddleware(AsyncMiddleware *middleware); + void addMiddlewares(std::vector<AsyncMiddleware *> middlewares); + bool removeMiddleware(AsyncMiddleware *middleware); + + // For internal use only + void _runChain(AsyncWebServerRequest *request, ArMiddlewareNext finalizer); + +protected: + std::list<AsyncMiddleware *> _middlewares; +}; + +// AsyncAuthenticationMiddleware is a middleware that checks if the request is authenticated +class AsyncAuthenticationMiddleware : 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; + } + + // set the authentication method to use + // default is AUTH_NONE: no authentication required + // AUTH_BASIC: basic authentication + // AUTH_DIGEST: digest authentication + // AUTH_BEARER: bearer token authentication + // AUTH_OTHER: other authentication method + // AUTH_DENIED: always return 401 Unauthorized + // if a method is set but no username or password is set, authentication will be ignored + void setAuthType(AsyncAuthType authMethod) { + _authMethod = authMethod; + } + + // precompute and store the hash value based on the username, password, realm. + // can be used for DIGEST and BASIC to avoid recomputing the hash for each request. + // returns true if the hash was successfully generated and replaced + bool generateHash(); + + // returns true if the username and password (or hash) are set + bool hasCredentials() const { + return _hasCreds; + } + + bool allowed(AsyncWebServerRequest *request) const; + + 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<bool(AsyncWebServerRequest *request)>; +// AsyncAuthorizationMiddleware is a middleware that checks if the request is authorized +class AsyncAuthorizationMiddleware : public AsyncMiddleware { +public: + AsyncAuthorizationMiddleware(ArAuthorizeFunction authorizeConnectHandler) : _code(403), _authz(authorizeConnectHandler) {} + AsyncAuthorizationMiddleware(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 AsyncHeaderFreeMiddleware : public AsyncMiddleware { +public: + void keep(const char *name) { + _toKeep.push_back(name); + } + void unKeep(const char *name) { + _toKeep.remove(name); + } + + void run(AsyncWebServerRequest *request, ArMiddlewareNext next); + +private: + std::list<const char *> _toKeep; +}; + +// filter out specific headers from the incoming request +class AsyncHeaderFilterMiddleware : public AsyncMiddleware { +public: + void filter(const char *name) { + _toRemove.push_back(name); + } + void unFilter(const char *name) { + _toRemove.remove(name); + } + + void run(AsyncWebServerRequest *request, ArMiddlewareNext next); + +private: + std::list<const char *> _toRemove; +}; + +// curl-like logging of incoming requests +class AsyncLoggingMiddleware : public AsyncMiddleware { +public: + void setOutput(Print &output) { + _out = &output; + } + void setEnabled(bool enabled) { + _enabled = enabled; + } + bool isEnabled() const { + return _enabled && _out; + } + + void run(AsyncWebServerRequest *request, ArMiddlewareNext next); + +private: + Print *_out = nullptr; + bool _enabled = true; +}; + +// CORS Middleware +class AsyncCorsMiddleware : 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 AsyncRateLimitMiddleware : 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<uint32_t> _requestTimes; +}; + +/* + * REWRITE :: One instance can be handle any Request (done by the Server) + * */ + +class AsyncWebRewrite { +protected: + String _from; + String _toUrl; + String _params; + ArRequestFilterFunction _filter{nullptr}; + +public: + AsyncWebRewrite(const char *from, const char *to) : _from(from), _toUrl(to) { + int index = _toUrl.indexOf('?'); + if (index > 0) { + _params = _toUrl.substring(index + 1); + _toUrl = _toUrl.substring(0, index); + } + } + virtual ~AsyncWebRewrite() {} + AsyncWebRewrite &setFilter(ArRequestFilterFunction fn) { + _filter = fn; + return *this; + } + bool filter(AsyncWebServerRequest *request) const { + return _filter == NULL || _filter(request); + } + const String &from(void) const { + return _from; + } + const String &toUrl(void) const { + return _toUrl; + } + const String ¶ms(void) const { + return _params; + } + virtual bool match(AsyncWebServerRequest *request) { + return from() == request->url() && filter(request); + } +}; + +/* + * HANDLER :: One instance can be attached to any Request (done by the Server) + * */ + +class AsyncWebHandler : public AsyncMiddlewareChain { +protected: + ArRequestFilterFunction _filter = nullptr; + AsyncAuthenticationMiddleware *_authMiddleware = nullptr; + bool _skipServerMiddlewares = false; + +public: + AsyncWebHandler() {} + virtual ~AsyncWebHandler() {} + AsyncWebHandler &setFilter(ArRequestFilterFunction fn); + AsyncWebHandler &setAuthentication(const char *username, const char *password, AsyncAuthType authMethod = AsyncAuthType::AUTH_DIGEST); + AsyncWebHandler &setAuthentication(const String &username, const String &password, AsyncAuthType authMethod = AsyncAuthType::AUTH_DIGEST) { + return setAuthentication(username.c_str(), password.c_str(), authMethod); + }; + AsyncWebHandler &setSkipServerMiddlewares(bool state) { + _skipServerMiddlewares = state; + return *this; + } + // skip all globally defined server middlewares for this handler and only execute those defined for this handler specifically + AsyncWebHandler &skipServerMiddlewares() { + return setSkipServerMiddlewares(true); + } + bool mustSkipServerMiddlewares() const { + return _skipServerMiddlewares; + } + bool filter(AsyncWebServerRequest *request) { + return _filter == NULL || _filter(request); + } + virtual bool canHandle(AsyncWebServerRequest *request __attribute__((unused))) const { + return false; + } + 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() const { + return true; + } +}; + +/* + * RESPONSE :: One instance is created for each Request (attached by the Handler) + * */ + +typedef enum { + RESPONSE_SETUP, + RESPONSE_HEADERS, + RESPONSE_CONTENT, + RESPONSE_WAIT_ACK, + RESPONSE_END, + RESPONSE_FAILED +} WebResponseState; + +class AsyncWebServerResponse { +protected: + int _code; + std::list<AsyncWebHeader> _headers; + String _contentType; + size_t _contentLength; + bool _sendContentLength; + bool _chunked; + size_t _headLength; + size_t _sentLength; + size_t _ackedLength; + size_t _writtenLength; + WebResponseState _state; + + static bool headerMustBePresentOnce(const String &name); + +public: + static const char *responseCodeToString(int code); + +public: + AsyncWebServerResponse(); + virtual ~AsyncWebServerResponse() {} + void setCode(int code); + int code() const { + return _code; + } + void setContentLength(size_t len); + void setContentType(const String &type) { + setContentType(type.c_str()); + } + void setContentType(const char *type); + bool addHeader(const char *name, const char *value, bool replaceExisting = true); + bool addHeader(const String &name, const String &value, bool replaceExisting = true) { + return addHeader(name.c_str(), value.c_str(), replaceExisting); + } + bool addHeader(const char *name, long value, bool replaceExisting = true) { + return addHeader(name, String(value), replaceExisting); + } + bool addHeader(const String &name, long value, bool replaceExisting = true) { + return addHeader(name.c_str(), value, replaceExisting); + } + bool removeHeader(const char *name); + bool removeHeader(const char *name, const char *value); + const AsyncWebHeader *getHeader(const char *name) const; + const std::list<AsyncWebHeader> &getHeaders() const { + return _headers; + } + +#ifndef ESP8266 + [[deprecated("Use instead: _assembleHead(String& buffer, uint8_t version)")]] +#endif + String _assembleHead(uint8_t version) { + String buffer; + _assembleHead(buffer, version); + return buffer; + } + void _assembleHead(String &buffer, uint8_t version); + + virtual bool _started() const; + virtual bool _finished() const; + virtual bool _failed() const; + virtual bool _sourceValid() const; + virtual void _respond(AsyncWebServerRequest *request); + virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); +}; + +/* + * SERVER :: One instance + * */ + +typedef std::function<void(AsyncWebServerRequest *request)> ArRequestHandlerFunction; +typedef std::function<void(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)> + ArUploadHandlerFunction; +typedef std::function<void(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)> ArBodyHandlerFunction; + +class AsyncWebServer : public AsyncMiddlewareChain { +protected: + AsyncServer _server; + std::list<std::shared_ptr<AsyncWebRewrite>> _rewrites; + std::list<std::unique_ptr<AsyncWebHandler>> _handlers; + AsyncCallbackWebHandler *_catchAllHandler; + +public: + AsyncWebServer(uint16_t port); + ~AsyncWebServer(); + + void begin(); + void end(); + + tcp_state state() const { +#ifdef ESP8266 + // ESPAsyncTCP and RPAsyncTCP methods are not corrected declared with const for immutable ones. + return static_cast<tcp_state>(const_cast<AsyncWebServer *>(this)->_server.status()); +#else + return static_cast<tcp_state>(_server.status()); +#endif + } + +#if ASYNC_TCP_SSL_ENABLED + void onSslFileRequest(AcSSlFileHandler cb, void *arg); + void beginSecure(const char *cert, const char *private_key_file, const char *password); +#endif + + AsyncWebRewrite &addRewrite(AsyncWebRewrite *rewrite); + + /** + * @brief (compat) Add url rewrite rule by pointer + * 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 + * @return AsyncWebRewrite& reference to a newly created rewrite rule + */ + AsyncWebRewrite &addRewrite(std::shared_ptr<AsyncWebRewrite> rewrite); + + /** + * @brief add url rewrite rule + * + * @param from + * @param to + * @return AsyncWebRewrite& + */ + AsyncWebRewrite &rewrite(const char *from, const char *to); + + /** + * @brief (compat) remove rewrite rule via referenced object + * this will NOT deallocate pointed object itself, internal rule with same from/to urls will be removed if any + * it's a compat method, better use `removeRewrite(const char* from, const char* to)` + * @param rewrite + * @return true + * @return false + */ + bool removeRewrite(AsyncWebRewrite *rewrite); + + /** + * @brief remove rewrite rule + * + * @param from + * @param to + * @return true + * @return false + */ + bool removeRewrite(const char *from, const char *to); + + AsyncWebHandler &addHandler(AsyncWebHandler *handler); + bool removeHandler(AsyncWebHandler *handler); + + 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); + + void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned + void onFileUpload(ArUploadHandlerFunction fn); // handle file uploads + void onRequestBody(ArBodyHandlerFunction fn); // handle posts with plain body content (JSON often transmitted this way as a request) + // give access to the handler used to catch all requests, so that middleware can be added to it + AsyncWebHandler &catchAllHandler() const; + + void reset(); // remove all writers and handlers, with onNotFound/onFileUpload/onRequestBody + + void _handleDisconnect(AsyncWebServerRequest *request); + void _attachHandler(AsyncWebServerRequest *request); + void _rewriteRequest(AsyncWebServerRequest *request); +}; + +class DefaultHeaders { + using headers_t = std::list<AsyncWebHeader>; + headers_t _headers; + +public: + DefaultHeaders() = default; + + using ConstIterator = headers_t::const_iterator; + + void addHeader(const String &name, const String &value) { + _headers.emplace_back(name, value); + } + + ConstIterator begin() const { + return _headers.begin(); + } + ConstIterator end() const { + return _headers.end(); + } + + DefaultHeaders(DefaultHeaders const &) = delete; + DefaultHeaders &operator=(DefaultHeaders const &) = delete; + + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } +}; + +#include "AsyncEventSource.h" +#include "AsyncWebSocket.h" +#include "WebHandlerImpl.h" +#include "WebResponseImpl.h" + +#endif /* _AsyncWebServer_H_ */ diff --git a/libraries/ESP_Async_WebServer/src/Middleware.cpp b/libraries/ESP_Async_WebServer/src/Middleware.cpp new file mode 100644 index 0000000..890303d --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/Middleware.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "WebAuthentication.h" +#include <ESPAsyncWebServer.h> + +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<AsyncMiddleware *> 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<AsyncMiddleware *>::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 AsyncAuthenticationMiddleware::setUsername(const char *username) { + _username = username; + _hasCreds = _username.length() && _credentials.length(); +} + +void AsyncAuthenticationMiddleware::setPassword(const char *password) { + _credentials = password; + _hash = false; + _hasCreds = _username.length() && _credentials.length(); +} + +void AsyncAuthenticationMiddleware::setPasswordHash(const char *hash) { + _credentials = hash; + _hash = _credentials.length(); + _hasCreds = _username.length() && _credentials.length(); +} + +bool AsyncAuthenticationMiddleware::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()); + if (_credentials.length()) { + _hash = true; + return true; + } else { + return false; + } + + case AsyncAuthType::AUTH_BASIC: + _credentials = generateBasicHash(_username.c_str(), _credentials.c_str()); + if (_credentials.length()) { + _hash = true; + return true; + } else { + return false; + } + + default: return false; + } +} + +bool AsyncAuthenticationMiddleware::allowed(AsyncWebServerRequest *request) const { + if (_authMethod == AsyncAuthType::AUTH_NONE) { + return true; + } + + if (_authMethod == AsyncAuthType::AUTH_DENIED) { + return false; + } + + if (!_hasCreds) { + return true; + } + + return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash); +} + +void AsyncAuthenticationMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) { + return allowed(request) ? next() : request->requestAuthentication(_authMethod, _realm.c_str(), _authFailMsg.c_str()); +} + +void AsyncHeaderFreeMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) { + std::list<const char *> toRemove; + for (auto &h : request->getHeaders()) { + bool keep = false; + for (const char *k : _toKeep) { + if (strcasecmp(h.name().c_str(), k) == 0) { + keep = true; + break; + } + } + if (!keep) { + toRemove.push_back(h.name().c_str()); + } + } + for (const char *h : toRemove) { + request->removeHeader(h); + } + next(); +} + +void AsyncHeaderFilterMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) { + for (auto it = _toRemove.begin(); it != _toRemove.end(); ++it) { + request->removeHeader(*it); + } + next(); +} + +void AsyncLoggingMiddleware::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 AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerResponse *response) { + response->addHeader(asyncsrv::T_CORS_ACAO, _origin.c_str()); + response->addHeader(asyncsrv::T_CORS_ACAM, _methods.c_str()); + response->addHeader(asyncsrv::T_CORS_ACAH, _headers.c_str()); + response->addHeader(asyncsrv::T_CORS_ACAC, _credentials ? asyncsrv::T_TRUE : asyncsrv::T_FALSE); + response->addHeader(asyncsrv::T_CORS_ACMA, String(_maxAge).c_str()); +} + +void AsyncCorsMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) { + // Origin header ? => CORS handling + if (request->hasHeader(asyncsrv::T_CORS_O)) { + // 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 AsyncRateLimitMiddleware::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 AsyncRateLimitMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) { + uint32_t retryAfterSeconds; + if (isRequestAllowed(retryAfterSeconds)) { + next(); + } else { + AsyncWebServerResponse *response = request->beginResponse(429); + response->addHeader(asyncsrv::T_retry_after, retryAfterSeconds); + request->send(response); + } +} diff --git a/libraries/ESP_Async_WebServer/src/WebAuthentication.cpp b/libraries/ESP_Async_WebServer/src/WebAuthentication.cpp new file mode 100644 index 0000000..7ed7814 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebAuthentication.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "WebAuthentication.h" +#include <libb64/cencode.h> +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include <MD5Builder.h> +#else +#include "md5.h" +#endif +#include "literals.h" + +using namespace asyncsrv; + +// Basic Auth hash = base64("username:password") + +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; + + char *toencode = new char[toencodeLen + 1]; + if (toencode == NULL) { + return emptyString; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; + if (encoded == NULL) { + delete[] toencode; + return emptyString; + } + sprintf_P(toencode, PSTR("%s:%s"), username, password); + if (base64_encode_chars(toencode, toencodeLen, encoded) > 0) { + String res = String(encoded); + delete[] toencode; + delete[] encoded; + return res; + } + delete[] toencode; + delete[] encoded; + return emptyString; +} + +static bool getMD5(uint8_t *data, uint16_t len, char *output) { // 33 bytes or more +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + MD5Builder md5; + md5.begin(); + md5.add(data, len); + md5.calculate(); + md5.getChars(output); +#else + md5_context_t _ctx; + + uint8_t *_buf = (uint8_t *)malloc(16); + if (_buf == NULL) { + return false; + } + memset(_buf, 0x00, 16); + + MD5Init(&_ctx); + MD5Update(&_ctx, data, len); + MD5Final(_buf, &_ctx); + + for (uint8_t i = 0; i < 16; i++) { + sprintf_P(output + (i * 2), PSTR("%02x"), _buf[i]); + } + + free(_buf); +#endif + return true; +} + +String genRandomMD5() { +#ifdef ESP8266 + uint32_t r = RANDOM_REG32; +#else + uint32_t r = rand(); +#endif + char *out = (char *)malloc(33); + if (out == NULL || !getMD5((uint8_t *)(&r), 4, out)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + return emptyString; + } + String res = String(out); + free(out); + return res; +} + +static String stringMD5(const String &in) { + char *out = (char *)malloc(33); + if (out == NULL || !getMD5((uint8_t *)(in.c_str()), in.length(), out)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + return emptyString; + } + String res = String(out); + free(out); + return res; +} + +String generateDigestHash(const char *username, const char *password, const char *realm) { + if (username == NULL || password == NULL || realm == NULL) { + return emptyString; + } + char *out = (char *)malloc(33); + if (out == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + return emptyString; + } + + String in; + if (!in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + free(out); + return emptyString; + } + + in.concat(username); + in.concat(':'); + in.concat(realm); + in.concat(':'); + in.concat(password); + + if (!getMD5((uint8_t *)(in.c_str()), in.length(), out)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + free(out); + return emptyString; + } + + in = String(out); + free(out); + return in; +} + +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 +) { + if (username == NULL || password == NULL || header == NULL || method == NULL) { + // os_printf("AUTH FAIL: missing required fields\n"); + return false; + } + + String myHeader(header); + int nextBreak = myHeader.indexOf(','); + if (nextBreak < 0) { + // os_printf("AUTH FAIL: no variables\n"); + return false; + } + + String myUsername; + String myRealm; + String myNonce; + String myUri; + String myResponse; + String myQop; + String myNc; + String myCnonce; + + myHeader += (char)0x2c; // ',' + myHeader += (char)0x20; // ' ' + do { + String avLine(myHeader.substring(0, nextBreak)); + avLine.trim(); + myHeader = myHeader.substring(nextBreak + 1); + nextBreak = myHeader.indexOf(','); + + int eqSign = avLine.indexOf('='); + if (eqSign < 0) { + // os_printf("AUTH FAIL: no = sign\n"); + return false; + } + String varName(avLine.substring(0, eqSign)); + avLine = avLine.substring(eqSign + 1); + if (avLine.startsWith(String('"'))) { + avLine = avLine.substring(1, avLine.length() - 1); + } + + if (varName.equals(T_username)) { + if (!avLine.equals(username)) { + // os_printf("AUTH FAIL: username\n"); + return false; + } + myUsername = avLine; + } else if (varName.equals(T_realm)) { + if (realm != NULL && !avLine.equals(realm)) { + // os_printf("AUTH FAIL: realm\n"); + return false; + } + myRealm = avLine; + } else if (varName.equals(T_nonce)) { + if (nonce != NULL && !avLine.equals(nonce)) { + // os_printf("AUTH FAIL: nonce\n"); + return false; + } + myNonce = avLine; + } else if (varName.equals(T_opaque)) { + if (opaque != NULL && !avLine.equals(opaque)) { + // os_printf("AUTH FAIL: opaque\n"); + return false; + } + } else if (varName.equals(T_uri)) { + if (uri != NULL && !avLine.equals(uri)) { + // os_printf("AUTH FAIL: uri\n"); + return false; + } + myUri = avLine; + } else if (varName.equals(T_response)) { + myResponse = avLine; + } else if (varName.equals(T_qop)) { + myQop = avLine; + } else if (varName.equals(T_nc)) { + myNc = avLine; + } else if (varName.equals(T_cnonce)) { + myCnonce = avLine; + } + } while (nextBreak > 0); + + 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"); + return true; + } + + // os_printf("AUTH FAIL: password\n"); + return false; +} diff --git a/libraries/ESP_Async_WebServer/src/WebAuthentication.h b/libraries/ESP_Async_WebServer/src/WebAuthentication.h new file mode 100644 index 0000000..1711821 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebAuthentication.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef WEB_AUTHENTICATION_H_ +#define WEB_AUTHENTICATION_H_ + +#include "Arduino.h" + +bool checkBasicAuthentication(const char *header, const char *username, const char *password); + +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 +); + +// 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/libraries/ESP_Async_WebServer/src/WebHandlerImpl.h b/libraries/ESP_Async_WebServer/src/WebHandlerImpl.h new file mode 100644 index 0000000..1f68d62 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebHandlerImpl.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNCWEBSERVERHANDLERIMPL_H_ +#define ASYNCWEBSERVERHANDLERIMPL_H_ + +#include <string> +#ifdef ASYNCWEBSERVER_REGEX +#include <regex> +#endif + +#include "stddef.h" +#include <time.h> + +class AsyncStaticWebHandler : public AsyncWebHandler { + using File = fs::File; + using FS = fs::FS; + +private: + bool _getFile(AsyncWebServerRequest *request) const; + bool _searchFile(AsyncWebServerRequest *request, const String &path); + +protected: + FS _fs; + String _uri; + String _path; + String _default_file; + String _cache_control; + String _last_modified; + AwsTemplateProcessor _callback; + bool _isDir; + bool _tryGzipFirst = true; + +public: + AsyncStaticWebHandler(const char *uri, FS &fs, const char *path, const char *cache_control); + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; + AsyncStaticWebHandler &setTryGzipFirst(bool value); + AsyncStaticWebHandler &setIsDir(bool isDir); + AsyncStaticWebHandler &setDefaultFile(const char *filename); + AsyncStaticWebHandler &setCacheControl(const char *cache_control); + + /** + * @brief Set the Last-Modified time for the object + * + * @param last_modified + * @return AsyncStaticWebHandler& + */ + AsyncStaticWebHandler &setLastModified(const char *last_modified); + AsyncStaticWebHandler &setLastModified(struct tm *last_modified); + AsyncStaticWebHandler &setLastModified(time_t last_modified); + // sets to current time. Make sure sntp is running and time is updated + AsyncStaticWebHandler &setLastModified(); + + AsyncStaticWebHandler &setTemplateProcessor(AwsTemplateProcessor newCallback); +}; + +class AsyncCallbackWebHandler : public AsyncWebHandler { +private: +protected: + String _uri; + WebRequestMethodComposite _method; + ArRequestHandlerFunction _onRequest; + ArUploadHandlerFunction _onUpload; + ArBodyHandlerFunction _onBody; + bool _isRegex; + +public: + AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} + 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; + } + + bool canHandle(AsyncWebServerRequest *request) const override final; + void handleRequest(AsyncWebServerRequest *request) override final; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override final; + void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final; + bool isRequestHandlerTrivial() const override final { + return !_onRequest; + } +}; + +#endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */ diff --git a/libraries/ESP_Async_WebServer/src/WebHandlers.cpp b/libraries/ESP_Async_WebServer/src/WebHandlers.cpp new file mode 100644 index 0000000..acfc7c0 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebHandlers.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "ESPAsyncWebServer.h" +#include "WebHandlerImpl.h" + +using namespace asyncsrv; + +AsyncWebHandler &AsyncWebHandler::setFilter(ArRequestFilterFunction fn) { + _filter = fn; + return *this; +} +AsyncWebHandler &AsyncWebHandler::setAuthentication(const char *username, const char *password, AsyncAuthType authMethod) { + if (!_authMiddleware) { + _authMiddleware = new AsyncAuthenticationMiddleware(); + _authMiddleware->_freeOnRemoval = true; + addMiddleware(_authMiddleware); + } + _authMiddleware->setUsername(username); + _authMiddleware->setPassword(password); + _authMiddleware->setAuthType(authMethod); + 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) { + // Ensure leading '/' + if (_uri.length() == 0 || _uri[0] != '/') { + _uri = String('/') + _uri; + } + if (_path.length() == 0 || _path[0] != '/') { + _path = String('/') + _path; + } + + // If path ends with '/' we assume a hint that this is a directory to improve performance. + // However - if it does not end with '/' we, can't assume a file, path can still be a directory. + _isDir = _path[_path.length() - 1] == '/'; + + // Remove the trailing '/' so we can handle default file + // Notice that root will be "" not "/" + if (_uri[_uri.length() - 1] == '/') { + _uri = _uri.substring(0, _uri.length() - 1); + } + if (_path[_path.length() - 1] == '/') { + _path = _path.substring(0, _path.length() - 1); + } +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setTryGzipFirst(bool value) { + _tryGzipFirst = value; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setIsDir(bool isDir) { + _isDir = isDir; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setDefaultFile(const char *filename) { + _default_file = filename; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setCacheControl(const char *cache_control) { + _cache_control = cache_control; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(const char *last_modified) { + _last_modified = last_modified; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(struct tm *last_modified) { + char result[30]; +#ifdef ESP8266 + auto formatP = PSTR("%a, %d %b %Y %H:%M:%S GMT"); + char format[strlen_P(formatP) + 1]; + strcpy_P(format, formatP); +#else + static constexpr const char *format = "%a, %d %b %Y %H:%M:%S GMT"; +#endif + + strftime(result, sizeof(result), format, last_modified); + _last_modified = result; + return *this; +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(time_t last_modified) { + return setLastModified((struct tm *)gmtime(&last_modified)); +} + +AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified() { + time_t last_modified; + if (time(&last_modified) == 0) { // time is not yet set + return *this; + } + return setLastModified(last_modified); +} + +bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest *request) const { + return request->isHTTP() && request->method() == HTTP_GET && request->url().startsWith(_uri) && _getFile(request); +} + +bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest *request) const { + // Remove the found uri + String path = request->url().substring(_uri.length()); + + // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/' + bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length() - 1] == '/'); + + path = _path + path; + + // Do we have a file or .gz file + if (!canSkipFileCheck && const_cast<AsyncStaticWebHandler *>(this)->_searchFile(request, path)) { + return true; + } + + // Can't handle if not default file + if (_default_file.length() == 0) { + return false; + } + + // Try to add default file, ensure there is a trailing '/' to the path. + if (path.length() == 0 || path[path.length() - 1] != '/') { + path += String('/'); + } + path += _default_file; + + return const_cast<AsyncStaticWebHandler *>(this)->_searchFile(request, path); +} + +#ifdef ESP32 +#define FILE_IS_REAL(f) (f == true && !f.isDirectory()) +#else +#define FILE_IS_REAL(f) (f == true) +#endif + +bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const String &path) { + bool fileFound = false; + bool gzipFound = false; + + String gzip = path + T__gz; + + if (_tryGzipFirst) { + if (_fs.exists(gzip)) { + request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read); + gzipFound = FILE_IS_REAL(request->_tempFile); + } + if (!gzipFound) { + if (_fs.exists(path)) { + request->_tempFile = _fs.open(path, fs::FileOpenMode::read); + fileFound = FILE_IS_REAL(request->_tempFile); + } + } + } else { + if (_fs.exists(path)) { + request->_tempFile = _fs.open(path, fs::FileOpenMode::read); + fileFound = FILE_IS_REAL(request->_tempFile); + } + if (!fileFound) { + if (_fs.exists(gzip)) { + request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read); + gzipFound = FILE_IS_REAL(request->_tempFile); + } + } + } + + bool found = fileFound || gzipFound; + + if (found) { + // Extract the file name from the path and keep it in _tempObject + size_t pathLen = path.length(); + char *_tempPath = (char *)malloc(pathLen + 1); + if (_tempPath == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + request->_tempFile.close(); + return false; + } + snprintf_P(_tempPath, pathLen + 1, PSTR("%s"), path.c_str()); + request->_tempObject = (void *)_tempPath; + } + + return found; +} + +void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { + // Get the filename from request->_tempObject and free it + String filename((char *)request->_tempObject); + free(request->_tempObject); + request->_tempObject = NULL; + + if (request->_tempFile != true) { + request->send(404); + return; + } + + time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS) + // set etag to lastmod timestamp if available, otherwise to size + String etag; + if (lw) { + setLastModified(lw); +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + // time_t == long long int + constexpr size_t len = 1 + 8 * sizeof(time_t); + char buf[len]; + char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10); + etag = ret ? String(ret) : String(request->_tempFile.size()); +#else + etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp +#endif + } else { +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + etag = String(request->_tempFile.size()); +#else + etag = request->_tempFile.size(); +#endif + } + + bool not_modified = false; + + // if-none-match has precedence over if-modified-since + if (request->hasHeader(T_INM)) { + not_modified = request->header(T_INM).equals(etag); + } else if (_last_modified.length()) { + not_modified = request->header(T_IMS).equals(_last_modified); + } + + AsyncWebServerResponse *response; + + if (not_modified) { + request->_tempFile.close(); + response = new AsyncBasicResponse(304); // Not modified + } else { + response = new AsyncFileResponse(request->_tempFile, filename, emptyString, false, _callback); + } + + if (!response) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return; + } + + response->addHeader(T_ETag, etag.c_str()); + + if (_last_modified.length()) { + response->addHeader(T_Last_Modified, _last_modified.c_str()); + } + if (_cache_control.length()) { + response->addHeader(T_Cache_Control, _cache_control.c_str()); + } + + request->send(response); +} + +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) const { + if (!_onRequest || !request->isHTTP() || !(_method & request->method())) { + return false; + } + +#ifdef ASYNCWEBSERVER_REGEX + if (_isRegex) { + std::regex pattern(_uri.c_str()); + std::smatch matches; + std::string s(request->url().c_str()); + if (std::regex_search(s, matches, pattern)) { + for (size_t i = 1; i < matches.size(); ++i) { // start from 1 + request->_addPathParam(matches[i].str().c_str()); + } + } else { + return false; + } + } else +#endif + if (_uri.length() && _uri.startsWith("/*.")) { + String uriTemplate = String(_uri); + uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf(".")); + if (!request->url().endsWith(uriTemplate)) { + return false; + } + } else if (_uri.length() && _uri.endsWith("*")) { + String uriTemplate = String(_uri); + uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1); + if (!request->url().startsWith(uriTemplate)) { + return false; + } + } else if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) { + return false; + } + + return true; +} + +void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) { + if (_onRequest) { + _onRequest(request); + } else { + request->send(404, T_text_plain, "Not found"); + } +} +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) { + // ESP_LOGD("AsyncWebServer", "AsyncCallbackWebHandler::handleBody"); + if (_onBody) { + _onBody(request, data, len, index, total); + } +} diff --git a/libraries/ESP_Async_WebServer/src/WebRequest.cpp b/libraries/ESP_Async_WebServer/src/WebRequest.cpp new file mode 100644 index 0000000..8b735af --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebRequest.cpp @@ -0,0 +1,1185 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "ESPAsyncWebServer.h" +#include "WebAuthentication.h" +#include "WebResponseImpl.h" +#include "literals.h" +#include <cstring> + +#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) + +static void doNotDelete(AsyncWebServerRequest *) {} + +using namespace asyncsrv; + +enum { + PARSE_REQ_START = 0, + PARSE_REQ_HEADERS = 1, + PARSE_REQ_BODY = 2, + PARSE_REQ_END = 3, + PARSE_REQ_FAIL = 4 +}; + +AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c) + : _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(), + _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), + _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), + _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { + c->onError( + [](void *r, AsyncClient *c, int8_t error) { + (void)c; + // log_e("AsyncWebServerRequest::_onError"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onError(error); + }, + this + ); + c->onAck( + [](void *r, AsyncClient *c, size_t len, uint32_t time) { + (void)c; + // log_e("AsyncWebServerRequest::_onAck"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onAck(len, time); + }, + this + ); + c->onDisconnect( + [](void *r, AsyncClient *c) { + // log_e("AsyncWebServerRequest::_onDisconnect"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onDisconnect(); + delete c; + }, + this + ); + c->onTimeout( + [](void *r, AsyncClient *c, uint32_t time) { + (void)c; + // log_e("AsyncWebServerRequest::_onTimeout"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onTimeout(time); + }, + this + ); + c->onData( + [](void *r, AsyncClient *c, void *buf, size_t len) { + (void)c; + // log_e("AsyncWebServerRequest::_onData"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onData(buf, len); + }, + this + ); + c->onPoll( + [](void *r, AsyncClient *c) { + (void)c; + // log_e("AsyncWebServerRequest::_onPoll"); + AsyncWebServerRequest *req = (AsyncWebServerRequest *)r; + req->_onPoll(); + }, + this + ); +} + +AsyncWebServerRequest::~AsyncWebServerRequest() { + // log_e("AsyncWebServerRequest::~AsyncWebServerRequest"); + + _this.reset(); + + _headers.clear(); + + _pathParams.clear(); + + AsyncWebServerResponse *r = _response; + _response = NULL; + delete r; + + if (_tempObject != NULL) { + free(_tempObject); + } + + if (_tempFile) { + _tempFile.close(); + } + + if (_itemBuffer) { + free(_itemBuffer); + } +} + +void AsyncWebServerRequest::_onData(void *buf, size_t len) { + // SSL/TLS handshake detection +#ifndef ASYNC_TCP_SSL_ENABLED + if (_parseState == PARSE_REQ_START && len && ((uint8_t *)buf)[0] == 0x16) { // 0x16 indicates a Handshake message (SSL/TLS). +#ifdef ESP32 + log_d("SSL/TLS handshake detected: resetting connection"); +#endif + _parseState = PARSE_REQ_FAIL; + abort(); + return; + } +#endif + + size_t i = 0; + while (true) { + + if (_parseState < PARSE_REQ_BODY) { + // Find new line in buf + char *str = (char *)buf; + for (i = 0; i < len; i++) { + // Check for null characters in header + if (!str[i]) { + _parseState = PARSE_REQ_FAIL; + abort(); + return; + } + if (str[i] == '\n') { + break; + } + } + if (i == len) { // No new line, just add the buffer in _temp + char ch = str[len - 1]; + str[len - 1] = 0; + if (!_temp.reserve(_temp.length() + len)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + _parseState = PARSE_REQ_FAIL; + abort(); + return; + } + _temp.concat(str); + _temp.concat(ch); + } else { // Found new line - extract it and parse + str[i] = 0; // Terminate the string at the end of the line. + _temp.concat(str); + _temp.trim(); + _parseLine(); + if (++i < len) { + // Still have more buffer to process + buf = str + i; + len -= i; + continue; + } + } + } else if (_parseState == PARSE_REQ_BODY) { + // A handler should be already attached at this point in _parseLine function. + // If handler does nothing (_onRequest is NULL), we don't need to really parse the body. + const bool needParse = _handler && !_handler->isRequestHandlerTrivial(); + // Discard any bytes after content length; handlers may overrun their buffers + len = std::min(len, _contentLength - _parsedLength); + if (_isMultipart) { + if (needParse) { + size_t i; + for (i = 0; i < len; i++) { + _parseMultipartPostByte(((uint8_t *)buf)[i], i == len - 1); + _parsedLength++; + } + } else { + _parsedLength += len; + } + } else { + if (_parsedLength == 0) { + if (_contentType.startsWith(T_app_xform_urlencoded)) { + _isPlainPost = true; + } else if (_contentType == T_text_plain && __is_param_char(((char *)buf)[0])) { + size_t i = 0; + while (i < len && __is_param_char(((char *)buf)[i++])); + if (i < len && ((char *)buf)[i - 1] == '=') { + _isPlainPost = true; + } + } + } + if (!_isPlainPost) { + // ESP_LOGD("AsyncWebServer", "_isPlainPost: %d, _handler: %p", _isPlainPost, _handler); + if (_handler) { + _handler->handleBody(this, (uint8_t *)buf, len, _parsedLength, _contentLength); + } + _parsedLength += len; + } else if (needParse) { + size_t i; + for (i = 0; i < len; i++) { + _parsedLength++; + _parsePlainPostChar(((uint8_t *)buf)[i]); + } + } else { + _parsedLength += len; + } + } + if (_parsedLength == _contentLength) { + _parseState = PARSE_REQ_END; + _runMiddlewareChain(); + _send(); + } + } + break; + } +} + +void AsyncWebServerRequest::_onPoll() { + // os_printf("p\n"); + if (_response != NULL && _client != NULL && _client->canSend()) { + if (!_response->_finished()) { + _response->_ack(this, 0, 0); + } else { + AsyncWebServerResponse *r = _response; + _response = NULL; + delete r; + + _client->close(); + } + } +} + +void AsyncWebServerRequest::_onAck(size_t len, uint32_t time) { + // os_printf("a:%u:%u\n", len, time); + if (_response != NULL) { + if (!_response->_finished()) { + _response->_ack(this, len, time); + } else if (_response->_finished()) { + AsyncWebServerResponse *r = _response; + _response = NULL; + delete r; + + _client->close(); + } + } +} + +void AsyncWebServerRequest::_onError(int8_t error) { + (void)error; +} + +void AsyncWebServerRequest::_onTimeout(uint32_t time) { + (void)time; + // os_printf("TIMEOUT: %u, state: %s\n", time, _client->stateToString()); + _client->close(); +} + +void AsyncWebServerRequest::onDisconnect(ArDisconnectHandler fn) { + _onDisconnectfn = fn; +} + +void AsyncWebServerRequest::_onDisconnect() { + // os_printf("d\n"); + if (_onDisconnectfn) { + _onDisconnectfn(); + } + _server->_handleDisconnect(this); +} + +void AsyncWebServerRequest::_addPathParam(const char *p) { + _pathParams.emplace_back(p); +} + +void AsyncWebServerRequest::_addGetParams(const String ¶ms) { + size_t start = 0; + while (start < params.length()) { + int end = params.indexOf('&', start); + if (end < 0) { + end = params.length(); + } + int equal = params.indexOf('=', start); + if (equal < 0 || equal > end) { + equal = end; + } + String name = urlDecode(params.substring(start, equal)); + String value = urlDecode(equal + 1 < end ? params.substring(equal + 1, end) : emptyString); + if (name.length()) { + _params.emplace_back(name, value); + } + start = end + 1; + } +} + +bool AsyncWebServerRequest::_parseReqHead() { + // Split the head into method, url and version + int index = _temp.indexOf(' '); + String m = _temp.substring(0, index); + index = _temp.indexOf(' ', index + 1); + String u = _temp.substring(m.length() + 1, index); + _temp = _temp.substring(index + 1); + + if (m == T_GET) { + _method = HTTP_GET; + } else if (m == T_POST) { + _method = HTTP_POST; + } else if (m == T_DELETE) { + _method = HTTP_DELETE; + } else if (m == T_PUT) { + _method = HTTP_PUT; + } else if (m == T_PATCH) { + _method = HTTP_PATCH; + } else if (m == T_HEAD) { + _method = HTTP_HEAD; + } else if (m == T_OPTIONS) { + _method = HTTP_OPTIONS; + } else { + return false; + } + + String g; + index = u.indexOf('?'); + if (index > 0) { + g = u.substring(index + 1); + u = u.substring(0, index); + } + _url = urlDecode(u); + _addGetParams(g); + + if (!_url.length()) { + return false; + } + + if (!_temp.startsWith(T_HTTP_1_0)) { + _version = 1; + } + + _temp = emptyString; + return true; +} + +bool AsyncWebServerRequest::_parseReqHeader() { + int index = _temp.indexOf(':'); + if (index) { + String name(_temp.substring(0, index)); + String value(_temp.substring(index + 2)); + if (name.equalsIgnoreCase(T_Host)) { + _host = value; + } else if (name.equalsIgnoreCase(T_Content_Type)) { + _contentType = value.substring(0, value.indexOf(';')); + if (value.startsWith(T_MULTIPART_)) { + _boundary = value.substring(value.indexOf('=') + 1); + _boundary.replace(String('"'), String()); + _isMultipart = true; + } + } else if (name.equalsIgnoreCase(T_Content_Length)) { + _contentLength = atoi(value.c_str()); + } else if (name.equalsIgnoreCase(T_EXPECT) && value.equalsIgnoreCase(T_100_CONTINUE)) { + _expectingContinue = true; + } else if (name.equalsIgnoreCase(T_AUTH)) { + int space = value.indexOf(' '); + if (space == -1) { + _authorization = value; + _authMethod = AsyncAuthType::AUTH_OTHER; + } else { + String method = value.substring(0, space); + if (method.equalsIgnoreCase(T_BASIC)) { + _authMethod = AsyncAuthType::AUTH_BASIC; + } else if (method.equalsIgnoreCase(T_DIGEST)) { + _authMethod = AsyncAuthType::AUTH_DIGEST; + } else if (method.equalsIgnoreCase(T_BEARER)) { + _authMethod = AsyncAuthType::AUTH_BEARER; + } else { + _authMethod = AsyncAuthType::AUTH_OTHER; + } + _authorization = value.substring(space + 1); + } + } else if (name.equalsIgnoreCase(T_UPGRADE) && value.equalsIgnoreCase(T_WS)) { + // WebSocket request can be uniquely identified by header: [Upgrade: websocket] + _reqconntype = RCT_WS; + } else if (name.equalsIgnoreCase(T_ACCEPT)) { + String lowcase(value); + lowcase.toLowerCase(); +#ifndef ESP8266 + const char *substr = std::strstr(lowcase.c_str(), T_text_event_stream); +#else + const char *substr = std::strstr(lowcase.c_str(), String(T_text_event_stream).c_str()); +#endif + if (substr != NULL) { + // WebEvent request can be uniquely identified by header: [Accept: text/event-stream] + _reqconntype = RCT_EVENT; + } + } + _headers.emplace_back(name, value); + } +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + // Ancient PRI core does not have String::clear() method 8-() + _temp = emptyString; +#else + _temp.clear(); +#endif + return true; +} + +void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { + if (data && (char)data != '&') { + _temp += (char)data; + } + if (!data || (char)data == '&' || _parsedLength == _contentLength) { + String name(T_BODY); + String value(_temp); + if (!(_temp.charAt(0) == '{') && !(_temp.charAt(0) == '[') && _temp.indexOf('=') > 0) { + name = _temp.substring(0, _temp.indexOf('=')); + value = _temp.substring(_temp.indexOf('=') + 1); + } + name = urlDecode(name); + if (name.length()) { + _params.emplace_back(name, urlDecode(value), true); + } + +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + // Ancient PRI core does not have String::clear() method 8-() + _temp = emptyString; +#else + _temp.clear(); +#endif + } +} + +void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last) { + _itemBuffer[_itemBufferIndex++] = data; + + 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); + } + _itemBufferIndex = 0; + } +} + +enum { + EXPECT_BOUNDARY, + PARSE_HEADERS, + WAIT_FOR_RETURN1, + EXPECT_FEED1, + EXPECT_DASH1, + EXPECT_DASH2, + BOUNDARY_OR_DATA, + DASH3_OR_RETURN2, + EXPECT_FEED2, + PARSING_FINISHED, + PARSE_ERROR +}; + +void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { +#define itemWriteByte(b) \ + do { \ + _itemSize++; \ + if (_itemIsFile) \ + _handleUploadByte(b, last); \ + else \ + _itemValue += (char)(b); \ + } while (0) + + if (!_parsedLength) { + _multiParseState = EXPECT_BOUNDARY; + _temp = emptyString; + _itemName = emptyString; + _itemFilename = emptyString; + _itemType = emptyString; + } + + if (_multiParseState == WAIT_FOR_RETURN1) { + if (data != '\r') { + itemWriteByte(data); + } else { + _multiParseState = EXPECT_FEED1; + } + } else if (_multiParseState == EXPECT_BOUNDARY) { + if (_parsedLength < 2 && data != '-') { + _multiParseState = PARSE_ERROR; + return; + } else if (_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data) { + _multiParseState = PARSE_ERROR; + return; + } else if (_parsedLength - 2 == _boundary.length() && data != '\r') { + _multiParseState = PARSE_ERROR; + return; + } else if (_parsedLength - 3 == _boundary.length()) { + if (data != '\n') { + _multiParseState = PARSE_ERROR; + return; + } + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } + } else if (_multiParseState == PARSE_HEADERS) { + if ((char)data != '\r' && (char)data != '\n') { + _temp += (char)data; + } + if ((char)data == '\n') { + if (_temp.length()) { + if (_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase(T_Content_Type)) { + _itemType = _temp.substring(14); + _itemIsFile = true; + } else if (_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase(T_Content_Disposition)) { + _temp = _temp.substring(_temp.indexOf(';') + 2); + while (_temp.indexOf(';') > 0) { + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); + if (name == T_name) { + _itemName = nameVal; + } else if (name == T_filename) { + _itemFilename = nameVal; + _itemIsFile = true; + } + _temp = _temp.substring(_temp.indexOf(';') + 2); + } + String name = _temp.substring(0, _temp.indexOf('=')); + String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); + if (name == T_name) { + _itemName = nameVal; + } else if (name == T_filename) { + _itemFilename = nameVal; + _itemIsFile = true; + } + } + _temp = emptyString; + } else { + _multiParseState = WAIT_FOR_RETURN1; + // value starts from here + _itemSize = 0; + _itemStartIndex = _parsedLength; + _itemValue = emptyString; + if (_itemIsFile) { + if (_itemBuffer) { + free(_itemBuffer); + } + _itemBuffer = (uint8_t *)malloc(RESPONSE_STREAM_BUFFER_SIZE); + if (_itemBuffer == NULL) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + _multiParseState = PARSE_ERROR; + abort(); + return; + } + _itemBufferIndex = 0; + } + } + } + } else if (_multiParseState == EXPECT_FEED1) { + if (data != '\n') { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH1; + } + } else if (_multiParseState == EXPECT_DASH1) { + if (data != '-') { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + itemWriteByte('\n'); + _parseMultipartPostByte(data, last); + } else { + _multiParseState = EXPECT_DASH2; + } + } else if (_multiParseState == EXPECT_DASH2) { + if (data != '-') { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + itemWriteByte('\n'); + itemWriteByte('-'); + _parseMultipartPostByte(data, last); + } else { + _multiParseState = BOUNDARY_OR_DATA; + _boundaryPosition = 0; + } + } else if (_multiParseState == BOUNDARY_OR_DATA) { + if (_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data) { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + itemWriteByte('\n'); + itemWriteByte('-'); + itemWriteByte('-'); + uint8_t i; + for (i = 0; i < _boundaryPosition; i++) { + itemWriteByte(_boundary.c_str()[i]); + } + _parseMultipartPostByte(data, last); + } else if (_boundaryPosition == _boundary.length() - 1) { + _multiParseState = DASH3_OR_RETURN2; + if (!_itemIsFile) { + _params.emplace_back(_itemName, _itemValue, true); + } else { + if (_itemSize) { + if (_handler) { + _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); + } + _itemBufferIndex = 0; + _params.emplace_back(_itemName, _itemFilename, true, true, _itemSize); + } + free(_itemBuffer); + _itemBuffer = NULL; + } + + } else { + _boundaryPosition++; + } + } else if (_multiParseState == DASH3_OR_RETURN2) { + if (data == '-' && (_contentLength - _parsedLength - 4) != 0) { + // os_printf("ERROR: The parser got to the end of the POST but is expecting %u bytes more!\nDrop an issue so we can have more info on the matter!\n", _contentLength - _parsedLength - 4); + _contentLength = _parsedLength + 4; // lets close the request gracefully + } + if (data == '\r') { + _multiParseState = EXPECT_FEED2; + } else if (data == '-' && _contentLength == (_parsedLength + 4)) { + _multiParseState = PARSING_FINISHED; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + itemWriteByte('\n'); + itemWriteByte('-'); + itemWriteByte('-'); + uint8_t i; + for (i = 0; i < _boundary.length(); i++) { + itemWriteByte(_boundary.c_str()[i]); + } + _parseMultipartPostByte(data, last); + } + } else if (_multiParseState == EXPECT_FEED2) { + if (data == '\n') { + _multiParseState = PARSE_HEADERS; + _itemIsFile = false; + } else { + _multiParseState = WAIT_FOR_RETURN1; + itemWriteByte('\r'); + itemWriteByte('\n'); + itemWriteByte('-'); + itemWriteByte('-'); + uint8_t i; + for (i = 0; i < _boundary.length(); i++) { + itemWriteByte(_boundary.c_str()[i]); + } + itemWriteByte('\r'); + _parseMultipartPostByte(data, last); + } + } +} + +void AsyncWebServerRequest::_parseLine() { + if (_parseState == PARSE_REQ_START) { + if (!_temp.length()) { + _parseState = PARSE_REQ_FAIL; + abort(); + } else { + if (_parseReqHead()) { + _parseState = PARSE_REQ_HEADERS; + } else { + _parseState = PARSE_REQ_FAIL; + abort(); + } + } + return; + } + + if (_parseState == PARSE_REQ_HEADERS) { + if (!_temp.length()) { + // end of headers + _server->_rewriteRequest(this); + _server->_attachHandler(this); + if (_expectingContinue) { + String response(T_HTTP_100_CONT); + _client->write(response.c_str(), response.length()); + } + if (_contentLength) { + _parseState = PARSE_REQ_BODY; + } else { + _parseState = PARSE_REQ_END; + _runMiddlewareChain(); + _send(); + } + } else { + _parseReqHeader(); + } + } +} + +void AsyncWebServerRequest::_runMiddlewareChain() { + if (_handler && _handler->mustSkipServerMiddlewares()) { + _handler->_runChain(this, [this]() { + _handler->handleRequest(this); + }); + } else { + _server->_runChain(this, [this]() { + if (_handler) { + _handler->_runChain(this, [this]() { + _handler->handleRequest(this); + }); + } + }); + } +} + +void AsyncWebServerRequest::_send() { + if (!_sent && !_paused) { + // log_d("AsyncWebServerRequest::_send()"); + + // user did not create a response ? + if (!_response) { + send(501, T_text_plain, "Handler did not handle the request"); + } + + // response is not valid ? + if (!_response->_sourceValid()) { + send(500, T_text_plain, "Invalid data in handler"); + } + + // here, we either have a response give nfrom user or one of the two above + _client->setRxTimeout(0); + _response->_respond(this); + _sent = true; + } +} + +AsyncWebServerRequestPtr AsyncWebServerRequest::pause() { + if (_paused) { + return _this; + } + client()->setRxTimeout(0); + // this shared ptr will hold the request pointer until it gets destroyed following a disconnect. + // this is just used as a holder providing weak observers, so the deleter is a no-op. + _this = std::shared_ptr<AsyncWebServerRequest>(this, doNotDelete); + _paused = true; + return _this; +} + +void AsyncWebServerRequest::abort() { + if (!_sent) { + _sent = true; + _paused = false; + _this.reset(); + // log_e("AsyncWebServerRequest::abort"); + _client->abort(); + } +} + +size_t AsyncWebServerRequest::headers() const { + return _headers.size(); +} + +bool AsyncWebServerRequest::hasHeader(const char *name) const { + for (const auto &h : _headers) { + if (h.name().equalsIgnoreCase(name)) { + return true; + } + } + return false; +} + +#ifdef ESP8266 +bool AsyncWebServerRequest::hasHeader(const __FlashStringHelper *data) const { + return hasHeader(String(data)); +} +#endif + +const AsyncWebHeader *AsyncWebServerRequest::getHeader(const char *name) const { + auto iter = std::find_if(std::begin(_headers), std::end(_headers), [&name](const AsyncWebHeader &header) { + return header.name().equalsIgnoreCase(name); + }); + return (iter == std::end(_headers)) ? nullptr : &(*iter); +} + +#ifdef ESP8266 +const AsyncWebHeader *AsyncWebServerRequest::getHeader(const __FlashStringHelper *data) const { + PGM_P p = reinterpret_cast<PGM_P>(data); + size_t n = strlen_P(p); + char *name = (char *)malloc(n + 1); + if (name) { + strcpy_P(name, p); + const AsyncWebHeader *result = getHeader(String(name)); + free(name); + return result; + } else { + return nullptr; + } +} +#endif + +const AsyncWebHeader *AsyncWebServerRequest::getHeader(size_t num) const { + if (num >= _headers.size()) { + return nullptr; + } + return &(*std::next(_headers.cbegin(), num)); +} + +size_t AsyncWebServerRequest::getHeaderNames(std::vector<const char *> &names) const { + const size_t size = names.size(); + for (const auto &h : _headers) { + names.push_back(h.name().c_str()); + } + return names.size() - 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(); +} + +bool AsyncWebServerRequest::hasParam(const char *name, bool post, bool file) const { + for (const auto &p : _params) { + if (p.name().equals(name) && p.isPost() == post && p.isFile() == file) { + return true; + } + } + return false; +} + +const AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name, bool post, bool file) const { + for (const auto &p : _params) { + if (p.name() == name && p.isPost() == post && p.isFile() == file) { + return &p; + } + } + return nullptr; +} + +#ifdef ESP8266 +const AsyncWebParameter *AsyncWebServerRequest::getParam(const __FlashStringHelper *data, bool post, bool file) const { + return getParam(String(data), post, file); +} +#endif + +const AsyncWebParameter *AsyncWebServerRequest::getParam(size_t num) const { + if (num >= _params.size()) { + return nullptr; + } + return &(*std::next(_params.cbegin(), num)); +} + +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) { + if (callback) { + return new AsyncProgmemResponse(code, contentType, (const uint8_t *)content, strlen(content), callback); + } + return new AsyncBasicResponse(code, contentType, content); +} + +AsyncWebServerResponse * + AsyncWebServerRequest::beginResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback) { + return new AsyncProgmemResponse(code, contentType, content, len, callback); +} + +AsyncWebServerResponse * + AsyncWebServerRequest::beginResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) { + if (fs.exists(path) || (!download && fs.exists(path + T__gz))) { + return new AsyncFileResponse(fs, path, contentType, download, callback); + } + return NULL; +} + +AsyncWebServerResponse * + AsyncWebServerRequest::beginResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) { + if (content == true) { + return new AsyncFileResponse(content, path, contentType, download, callback); + } + return NULL; +} + +AsyncWebServerResponse *AsyncWebServerRequest::beginResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback) { + return new AsyncStreamResponse(stream, contentType, len, callback); +} + +AsyncWebServerResponse * + AsyncWebServerRequest::beginResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) { + return new AsyncCallbackResponse(contentType, len, callback, templateCallback); +} + +AsyncWebServerResponse * + AsyncWebServerRequest::beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) { + if (_version) { + return new AsyncChunkedResponse(contentType, callback, templateCallback); + } + return new AsyncCallbackResponse(contentType, 0, callback, templateCallback); +} + +AsyncResponseStream *AsyncWebServerRequest::beginResponseStream(const char *contentType, size_t bufferSize) { + return new AsyncResponseStream(contentType, bufferSize); +} + +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); +} + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { + // request is already sent on the wire ? + if (_sent) { + return; + } + + // if we already had a response, delete it and replace it with the new one + if (_response) { + delete _response; + } + _response = response; + + // if request was paused, we need to send the response now + if (_paused) { + _paused = false; + _send(); + } +} + +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) const { + if (_authorization.length()) { + 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); + } else { + return _authorization.equals(password); + } + } + return false; +} + +bool AsyncWebServerRequest::authenticate(const char *hash) const { + if (!_authorization.length() || hash == NULL) { + return false; + } + + if (_authMethod == AsyncAuthType::AUTH_DIGEST) { + String hStr = String(hash); + int separator = hStr.indexOf(':'); + if (separator <= 0) { + return false; + } + String username = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + separator = hStr.indexOf(':'); + if (separator <= 0) { + return false; + } + String realm = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + 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(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; + if (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()); + } else { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + abort(); + } + + break; + } + case AsyncAuthType::AUTH_DIGEST: + { + size_t len = strlen(T_DIGEST_) + strlen(T_realm__) + strlen(T_auth_nonce) + 32 + strlen(T__opaque) + 32 + 1; + String header; + if (header.reserve(len + strlen(realm))) { + const String nonce = genRandomMD5(); + const String opaque = genRandomMD5(); + if (nonce.length() && opaque.length()) { + header.concat(T_DIGEST_); + header.concat(T_realm__); + header.concat(realm); + header.concat(T_auth_nonce); + header.concat(nonce); + header.concat(T__opaque); + header.concat(opaque); + header.concat((char)0x22); // '"' + r->addHeader(T_WWW_AUTH, header.c_str()); + } else { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + abort(); + } + } + break; + } + default: break; + } + + send(r); +} + +bool AsyncWebServerRequest::hasArg(const char *name) const { + for (const auto &arg : _params) { + if (arg.name() == name) { + return true; + } + } + return false; +} + +#ifdef ESP8266 +bool AsyncWebServerRequest::hasArg(const __FlashStringHelper *data) const { + return hasArg(String(data).c_str()); +} +#endif + +const String &AsyncWebServerRequest::arg(const char *name) const { + for (const auto &arg : _params) { + if (arg.name() == name) { + return arg.value(); + } + } + return emptyString; +} + +#ifdef ESP8266 +const String &AsyncWebServerRequest::arg(const __FlashStringHelper *data) const { + return arg(String(data).c_str()); +} +#endif + +const String &AsyncWebServerRequest::arg(size_t i) const { + return getParam(i)->value(); +} + +const String &AsyncWebServerRequest::argName(size_t i) const { + return getParam(i)->name(); +} + +const String &AsyncWebServerRequest::pathArg(size_t i) const { + if (i >= _pathParams.size()) { + return emptyString; + } + auto it = _pathParams.begin(); + std::advance(it, i); + return *it; +} + +const String &AsyncWebServerRequest::header(const char *name) const { + const AsyncWebHeader *h = getHeader(name); + return h ? h->value() : emptyString; +} + +#ifdef ESP8266 +const String &AsyncWebServerRequest::header(const __FlashStringHelper *data) const { + return header(String(data).c_str()); +}; +#endif + +const String &AsyncWebServerRequest::header(size_t i) const { + const AsyncWebHeader *h = getHeader(i); + return h ? h->value() : emptyString; +} + +const String &AsyncWebServerRequest::headerName(size_t i) const { + const AsyncWebHeader *h = getHeader(i); + return h ? h->name() : emptyString; +} + +String AsyncWebServerRequest::urlDecode(const String &text) const { + char temp[] = "0x00"; + unsigned int len = text.length(); + unsigned int i = 0; + String decoded; + // Allocate the string internal buffer - never longer from source text + if (!decoded.reserve(len)) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + return emptyString; + } + while (i < len) { + char decodedChar; + char encodedChar = text.charAt(i++); + if ((encodedChar == '%') && (i + 1 < len)) { + temp[2] = text.charAt(i++); + temp[3] = text.charAt(i++); + decodedChar = strtol(temp, NULL, 16); + } else if (encodedChar == '+') { + decodedChar = ' '; + } else { + decodedChar = encodedChar; // normal ascii char + } + decoded.concat(decodedChar); + } + return decoded; +} + +const char *AsyncWebServerRequest::methodToString() const { + if (_method == HTTP_ANY) { + return T_ANY; + } + if (_method & HTTP_GET) { + return T_GET; + } + if (_method & HTTP_POST) { + return T_POST; + } + if (_method & HTTP_DELETE) { + return T_DELETE; + } + if (_method & HTTP_PUT) { + return T_PUT; + } + if (_method & HTTP_PATCH) { + return T_PATCH; + } + if (_method & HTTP_HEAD) { + return T_HEAD; + } + if (_method & HTTP_OPTIONS) { + return T_OPTIONS; + } + return T_UNKNOWN; +} + +const char *AsyncWebServerRequest::requestedConnTypeToString() const { + switch (_reqconntype) { + case RCT_NOT_USED: return T_RCT_NOT_USED; + case RCT_DEFAULT: return T_RCT_DEFAULT; + case RCT_HTTP: return T_RCT_HTTP; + case RCT_WS: return T_RCT_WS; + case RCT_EVENT: return T_RCT_EVENT; + default: return T_ERROR; + } +} + +bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType erct1, RequestedConnectionType erct2, RequestedConnectionType erct3) const { + return ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) || ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype)) + || ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype)); +} diff --git a/libraries/ESP_Async_WebServer/src/WebResponseImpl.h b/libraries/ESP_Async_WebServer/src/WebResponseImpl.h new file mode 100644 index 0000000..6408625 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebResponseImpl.h @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#ifndef ASYNCWEBSERVERRESPONSEIMPL_H_ +#define ASYNCWEBSERVERRESPONSEIMPL_H_ + +#ifdef Arduino_h +// arduino is not compatible with std::vector +#undef min +#undef max +#endif +#include "literals.h" +#include <cbuf.h> +#include <memory> +#include <vector> + +// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max. + +class AsyncBasicResponse : public AsyncWebServerResponse { +private: + String _content; + +public: + explicit AsyncBasicResponse(int code, const char *contentType = asyncsrv::empty, const char *content = asyncsrv::empty); + AsyncBasicResponse(int code, const String &contentType, const String &content = emptyString) + : AsyncBasicResponse(code, contentType.c_str(), content.c_str()) {} + void _respond(AsyncWebServerRequest *request) override final; + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final; + bool _sourceValid() const override final { + return true; + } +}; + +class AsyncAbstractResponse : public AsyncWebServerResponse { +private: +#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT + // amount of response data in-flight, i.e. sent, but not acked yet + size_t _in_flight{0}; + // in-flight queue credits + size_t _in_flight_credit{2}; +#endif + String _head; + // Data is inserted into cache at begin(). + // This is inefficient with vector, but if we use some other container, + // we won't be able to access it as contiguous array of bytes when reading from it, + // so by gaining performance in one place, we'll lose it in another. + std::vector<uint8_t> _cache; + size_t _readDataFromCacheOrContent(uint8_t *data, const size_t len); + size_t _fillBufferAndProcessTemplates(uint8_t *buf, size_t maxLen); + +protected: + AwsTemplateProcessor _callback; + +public: + AsyncAbstractResponse(AwsTemplateProcessor callback = nullptr); + virtual ~AsyncAbstractResponse() {} + void _respond(AsyncWebServerRequest *request) override final; + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final; + virtual bool _sourceValid() const { + return false; + } + virtual size_t _fillBuffer(uint8_t *buf __attribute__((unused)), size_t maxLen __attribute__((unused))) { + return 0; + } +}; + +#ifndef TEMPLATE_PLACEHOLDER +#define TEMPLATE_PLACEHOLDER '%' +#endif + +#define TEMPLATE_PARAM_NAME_LENGTH 32 +class AsyncFileResponse : public AsyncAbstractResponse { + using File = fs::File; + using FS = fs::FS; + +private: + File _content; + String _path; + void _setContentTypeFromPath(const String &path); + +public: + AsyncFileResponse(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); + AsyncFileResponse(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) + : AsyncFileResponse(fs, path, contentType.c_str(), download, callback) {} + AsyncFileResponse( + File content, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr + ); + AsyncFileResponse(File content, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) + : AsyncFileResponse(content, path, contentType.c_str(), download, callback) {} + ~AsyncFileResponse() { + _content.close(); + } + bool _sourceValid() const override final { + return !!(_content); + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; +}; + +class AsyncStreamResponse : public AsyncAbstractResponse { +private: + Stream *_content; + +public: + AsyncStreamResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback = nullptr); + AsyncStreamResponse(Stream &stream, const String &contentType, size_t len, AwsTemplateProcessor callback = nullptr) + : AsyncStreamResponse(stream, contentType.c_str(), len, callback) {} + bool _sourceValid() const override final { + return !!(_content); + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; +}; + +class AsyncCallbackResponse : public AsyncAbstractResponse { +private: + AwsResponseFiller _content; + size_t _filledLength; + +public: + AsyncCallbackResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); + AsyncCallbackResponse(const String &contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) + : AsyncCallbackResponse(contentType.c_str(), len, callback, templateCallback) {} + bool _sourceValid() const override final { + return !!(_content); + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; +}; + +class AsyncChunkedResponse : public AsyncAbstractResponse { +private: + AwsResponseFiller _content; + size_t _filledLength; + +public: + AsyncChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); + AsyncChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) + : AsyncChunkedResponse(contentType.c_str(), callback, templateCallback) {} + bool _sourceValid() const override final { + return !!(_content); + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; +}; + +class AsyncProgmemResponse : public AsyncAbstractResponse { +private: + const uint8_t *_content; + size_t _readLength; + +public: + AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr); + AsyncProgmemResponse(int code, const String &contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr) + : AsyncProgmemResponse(code, contentType.c_str(), content, len, callback) {} + bool _sourceValid() const override final { + return true; + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; +}; + +class AsyncResponseStream : public AsyncAbstractResponse, public Print { +private: + std::unique_ptr<cbuf> _content; + +public: + AsyncResponseStream(const char *contentType, size_t bufferSize); + AsyncResponseStream(const String &contentType, size_t bufferSize) : AsyncResponseStream(contentType.c_str(), bufferSize) {} + bool _sourceValid() const override final { + return (_state < RESPONSE_END); + } + size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; + size_t write(const uint8_t *data, size_t len); + size_t write(uint8_t data); + /** + * @brief Returns the number of bytes available in the stream. + */ + size_t available() const { + return _content->available(); + } + using Print::write; +}; + +#endif /* ASYNCWEBSERVERRESPONSEIMPL_H_ */ diff --git a/libraries/ESP_Async_WebServer/src/WebResponses.cpp b/libraries/ESP_Async_WebServer/src/WebResponses.cpp new file mode 100644 index 0000000..3de8f32 --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebResponses.cpp @@ -0,0 +1,859 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "ESPAsyncWebServer.h" +#include "WebResponseImpl.h" + +using namespace asyncsrv; + +// Since ESP8266 does not link memchr by default, here's its implementation. +void *memchr(void *ptr, int ch, size_t count) { + unsigned char *p = static_cast<unsigned char *>(ptr); + while (count--) { + if (*p++ == static_cast<unsigned char>(ch)) { + return --p; + } + } + return nullptr; +} + +/* + * Abstract Response + * + */ + +const char *AsyncWebServerResponse::responseCodeToString(int code) { + switch (code) { + case 100: return T_HTTP_CODE_100; + case 101: return T_HTTP_CODE_101; + case 200: return T_HTTP_CODE_200; + case 201: return T_HTTP_CODE_201; + case 202: return T_HTTP_CODE_202; + case 203: return T_HTTP_CODE_203; + case 204: return T_HTTP_CODE_204; + case 205: return T_HTTP_CODE_205; + case 206: return T_HTTP_CODE_206; + case 300: return T_HTTP_CODE_300; + case 301: return T_HTTP_CODE_301; + case 302: return T_HTTP_CODE_302; + case 303: return T_HTTP_CODE_303; + case 304: return T_HTTP_CODE_304; + case 305: return T_HTTP_CODE_305; + case 307: return T_HTTP_CODE_307; + case 400: return T_HTTP_CODE_400; + case 401: return T_HTTP_CODE_401; + case 402: return T_HTTP_CODE_402; + case 403: return T_HTTP_CODE_403; + case 404: return T_HTTP_CODE_404; + case 405: return T_HTTP_CODE_405; + case 406: return T_HTTP_CODE_406; + case 407: return T_HTTP_CODE_407; + case 408: return T_HTTP_CODE_408; + case 409: return T_HTTP_CODE_409; + case 410: return T_HTTP_CODE_410; + case 411: return T_HTTP_CODE_411; + case 412: return T_HTTP_CODE_412; + case 413: return T_HTTP_CODE_413; + case 414: return T_HTTP_CODE_414; + case 415: return T_HTTP_CODE_415; + case 416: return T_HTTP_CODE_416; + case 417: return T_HTTP_CODE_417; + case 429: return T_HTTP_CODE_429; + case 500: return T_HTTP_CODE_500; + case 501: return T_HTTP_CODE_501; + case 502: return T_HTTP_CODE_502; + case 503: return T_HTTP_CODE_503; + case 504: return T_HTTP_CODE_504; + case 505: return T_HTTP_CODE_505; + default: return T_HTTP_CODE_ANY; + } +} + +AsyncWebServerResponse::AsyncWebServerResponse() + : _code(0), _contentType(), _contentLength(0), _sendContentLength(true), _chunked(false), _headLength(0), _sentLength(0), _ackedLength(0), _writtenLength(0), + _state(RESPONSE_SETUP) { + for (const auto &header : DefaultHeaders::Instance()) { + _headers.emplace_back(header); + } +} + +void AsyncWebServerResponse::setCode(int code) { + if (_state == RESPONSE_SETUP) { + _code = code; + } +} + +void AsyncWebServerResponse::setContentLength(size_t len) { + if (_state == RESPONSE_SETUP && addHeader(T_Content_Length, len, true)) { + _contentLength = len; + } +} + +void AsyncWebServerResponse::setContentType(const char *type) { + if (_state == RESPONSE_SETUP && addHeader(T_Content_Type, type, true)) { + _contentType = type; + } +} + +bool AsyncWebServerResponse::removeHeader(const char *name) { + bool h_erased = false; + for (auto i = _headers.begin(); i != _headers.end();) { + if (i->name().equalsIgnoreCase(name)) { + _headers.erase(i); + h_erased = true; + } else { + ++i; + } + } + return h_erased; +} + +bool AsyncWebServerResponse::removeHeader(const char *name, const char *value) { + for (auto i = _headers.begin(); i != _headers.end(); ++i) { + if (i->name().equalsIgnoreCase(name) && i->value().equalsIgnoreCase(value)) { + _headers.erase(i); + return true; + } + } + return false; +} + +const AsyncWebHeader *AsyncWebServerResponse::getHeader(const char *name) const { + auto iter = std::find_if(std::begin(_headers), std::end(_headers), [&name](const AsyncWebHeader &header) { + return header.name().equalsIgnoreCase(name); + }); + return (iter == std::end(_headers)) ? nullptr : &(*iter); +} + +bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) { + for (uint8_t i = 0; i < T_only_once_headers_len; i++) { + if (name.equalsIgnoreCase(T_only_once_headers[i])) { + return true; + } + } + return false; +} + +bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) { + for (auto i = _headers.begin(); i != _headers.end(); ++i) { + if (i->name().equalsIgnoreCase(name)) { + // header already set + if (replaceExisting) { + // remove, break and add the new one + _headers.erase(i); + break; + } else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name + // do not update + return false; + } else { + break; // accept multiple headers with the same name + } + } + } + // header was not found found, or existing one was removed + _headers.emplace_back(name, value); + return true; +} + +void AsyncWebServerResponse::_assembleHead(String &buffer, uint8_t version) { + if (version) { + addHeader(T_Accept_Ranges, T_none, false); + if (_chunked) { + addHeader(T_Transfer_Encoding, T_chunked, false); + } + } + + if (_sendContentLength) { + addHeader(T_Content_Length, String(_contentLength), false); + } + + if (_contentType.length()) { + addHeader(T_Content_Type, _contentType.c_str(), false); + } + + // precompute buffer size to avoid reallocations by String class + size_t len = 0; + len += 50; // HTTP/1.1 200 <reason>\r\n + for (const auto &header : _headers) { + len += header.name().length() + header.value().length() + 4; + } + + // prepare buffer + buffer.reserve(len); + + // HTTP header +#ifdef ESP8266 + buffer.concat(PSTR("HTTP/1.")); +#else + buffer.concat("HTTP/1."); +#endif + buffer.concat(version); + buffer.concat(' '); + buffer.concat(_code); + buffer.concat(' '); + buffer.concat(responseCodeToString(_code)); + buffer.concat(T_rn); + + // Add headers + for (const auto &header : _headers) { + buffer.concat(header.name()); +#ifdef ESP8266 + buffer.concat(PSTR(": ")); +#else + buffer.concat(": "); +#endif + buffer.concat(header.value()); + buffer.concat(T_rn); + } + + buffer.concat(T_rn); + _headLength = buffer.length(); +} + +bool AsyncWebServerResponse::_started() const { + return _state > RESPONSE_SETUP; +} +bool AsyncWebServerResponse::_finished() const { + return _state > RESPONSE_WAIT_ACK; +} +bool AsyncWebServerResponse::_failed() const { + return _state == RESPONSE_FAILED; +} +bool AsyncWebServerResponse::_sourceValid() const { + return false; +} +void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request) { + _state = RESPONSE_END; + request->client()->close(); +} +size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) { + (void)request; + (void)len; + (void)time; + return 0; +} + +/* + * String/Code Response + * */ +AsyncBasicResponse::AsyncBasicResponse(int code, const char *contentType, const char *content) { + _code = code; + _content = content; + _contentType = contentType; + if (_content.length()) { + _contentLength = _content.length(); + if (!_contentType.length()) { + _contentType = T_text_plain; + } + } + addHeader(T_Connection, T_close, false); +} + +void AsyncBasicResponse::_respond(AsyncWebServerRequest *request) { + _state = RESPONSE_HEADERS; + String out; + _assembleHead(out, request->version()); + size_t outLen = out.length(); + size_t space = request->client()->space(); + if (!_contentLength && space >= outLen) { + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if (_contentLength && space >= outLen + _contentLength) { + out += _content; + outLen += _contentLength; + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_WAIT_ACK; + } else if (space && space < outLen) { + String partial = out.substring(0, space); + _content = out.substring(space) + _content; + _contentLength += outLen - space; + _writtenLength += request->client()->write(partial.c_str(), partial.length()); + _state = RESPONSE_CONTENT; + } else if (space > outLen && space < (outLen + _contentLength)) { + size_t shift = space - outLen; + outLen += shift; + _sentLength += shift; + out += _content.substring(0, shift); + _content = _content.substring(shift); + _writtenLength += request->client()->write(out.c_str(), outLen); + _state = RESPONSE_CONTENT; + } else { + _content = out + _content; + _contentLength += outLen; + _state = RESPONSE_CONTENT; + } +} + +size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) { + (void)time; + _ackedLength += len; + if (_state == RESPONSE_CONTENT) { + size_t available = _contentLength - _sentLength; + size_t space = request->client()->space(); + // we can fit in this packet + if (space > available) { + _writtenLength += request->client()->write(_content.c_str(), available); + _content = emptyString; + _state = RESPONSE_WAIT_ACK; + return available; + } + // send some data, the rest on ack + String out = _content.substring(0, space); + _content = _content.substring(space); + _sentLength += space; + _writtenLength += request->client()->write(out.c_str(), space); + return space; + } else if (_state == RESPONSE_WAIT_ACK) { + if (_ackedLength >= _writtenLength) { + _state = RESPONSE_END; + } + } + return 0; +} + +/* + * Abstract Response + * */ + +AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _callback(callback) { + // In case of template processing, we're unable to determine real response size + if (callback) { + _contentLength = 0; + _sendContentLength = false; + _chunked = true; + } +} + +void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request) { + addHeader(T_Connection, T_close, false); + _assembleHead(_head, request->version()); + _state = RESPONSE_HEADERS; + _ack(request, 0, 0); +} + +size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) { + (void)time; + if (!_sourceValid()) { + _state = RESPONSE_FAILED; + request->client()->close(); + return 0; + } + +#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT + // return a credit for each chunk of acked data (polls does not give any credits) + if (len) { + ++_in_flight_credit; + } + + // for chunked responses ignore acks if there are no _in_flight_credits left + if (_chunked && !_in_flight_credit) { +#ifdef ESP32 + log_d("(chunk) out of in-flight credits"); +#endif + return 0; + } + + _in_flight -= (_in_flight > len) ? len : _in_flight; + // get the size of available sock space +#endif + + _ackedLength += len; + size_t space = request->client()->space(); + + size_t headLen = _head.length(); + if (_state == RESPONSE_HEADERS) { + if (space >= headLen) { + _state = RESPONSE_CONTENT; + space -= headLen; + } else { + String out = _head.substring(0, space); + _head = _head.substring(space); + _writtenLength += request->client()->write(out.c_str(), out.length()); +#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT + _in_flight += out.length(); + --_in_flight_credit; // take a credit +#endif + return out.length(); + } + } + + if (_state == RESPONSE_CONTENT) { +#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT + // for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency, + // but flood asynctcp's queue and fragment socket buffer space for large responses. + // Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space. + // That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q + if (_in_flight > space) { + // log_d("defer user call %u/%u", _in_flight, space); + // take the credit back since we are ignoring this ack and rely on other inflight data + if (len) { + --_in_flight_credit; + } + return 0; + } +#endif + + size_t outLen; + if (_chunked) { + if (space <= 8) { + return 0; + } + + outLen = space; + } else if (!_sendContentLength) { + outLen = space; + } else { + outLen = ((_contentLength - _sentLength) > space) ? space : (_contentLength - _sentLength); + } + + uint8_t *buf = (uint8_t *)malloc(outLen + headLen); + if (!buf) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + request->abort(); + return 0; + } + + if (headLen) { + memcpy(buf, _head.c_str(), _head.length()); + } + + size_t readLen = 0; + + if (_chunked) { + // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. + // See RFC2616 sections 2, 3.6.1. + readLen = _fillBufferAndProcessTemplates(buf + headLen + 6, outLen - 8); + if (readLen == RESPONSE_TRY_AGAIN) { + free(buf); + return 0; + } + outLen = sprintf((char *)buf + headLen, "%04x", readLen) + headLen; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + outLen += readLen; + buf[outLen++] = '\r'; + buf[outLen++] = '\n'; + } else { + readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen); + if (readLen == RESPONSE_TRY_AGAIN) { + free(buf); + return 0; + } + outLen = readLen + headLen; + } + + if (headLen) { + _head = emptyString; + } + + if (outLen) { + _writtenLength += request->client()->write((const char *)buf, outLen); +#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT + _in_flight += outLen; + --_in_flight_credit; // take a credit +#endif + } + + if (_chunked) { + _sentLength += readLen; + } else { + _sentLength += outLen - headLen; + } + + free(buf); + + if ((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)) { + _state = RESPONSE_WAIT_ACK; + } + return outLen; + + } else if (_state == RESPONSE_WAIT_ACK) { + if (!_sendContentLength || _ackedLength >= _writtenLength) { + _state = RESPONSE_END; + if (!_chunked && !_sendContentLength) { + request->client()->close(true); + } + } + } + return 0; +} + +size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t *data, const size_t len) { + // If we have something in cache, copy it to buffer + const size_t readFromCache = std::min(len, _cache.size()); + if (readFromCache) { + memcpy(data, _cache.data(), readFromCache); + _cache.erase(_cache.begin(), _cache.begin() + readFromCache); + } + // If we need to read more... + const size_t needFromFile = len - readFromCache; + const size_t readFromContent = _fillBuffer(data + readFromCache, needFromFile); + return readFromCache + readFromContent; +} + +size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size_t len) { + if (!_callback) { + return _fillBuffer(data, len); + } + + const size_t originalLen = len; + len = _readDataFromCacheOrContent(data, len); + // Now we've read 'len' bytes, either from cache or from file + // Search for template placeholders + uint8_t *pTemplateStart = data; + while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1)) + ) { // data[0] ... data[len - 1] + uint8_t *pTemplateEnd = + (pTemplateStart < &data[len - 1]) ? (uint8_t *)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr; + // temporary buffer to hold parameter name + uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1]; + String paramName; + // If closing placeholder is found: + if (pTemplateEnd) { + // prepare argument to callback + const size_t paramNameLength = std::min((size_t)sizeof(buf) - 1, (size_t)(pTemplateEnd - pTemplateStart - 1)); + if (paramNameLength) { + memcpy(buf, pTemplateStart + 1, paramNameLength); + buf[paramNameLength] = 0; + paramName = String(reinterpret_cast<char *>(buf)); + } else { // double percent sign encountered, this is single percent sign escaped. + // remove the 2nd percent sign + memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1; + ++pTemplateStart; + } + } else if (&data[len - 1] - pTemplateStart + 1 + < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data + memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart); + const size_t readFromCacheOrContent = + _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1)); + if (readFromCacheOrContent) { + pTemplateEnd = (uint8_t *)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent); + if (pTemplateEnd) { + // prepare argument to callback + *pTemplateEnd = 0; + paramName = String(reinterpret_cast<char *>(buf)); + // Copy remaining read-ahead data into cache + _cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + pTemplateEnd = &data[len - 1]; + } else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position + { + // but first, store read file data in cache + _cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent); + ++pTemplateStart; + } + } else { // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + } + } else { // closing placeholder not found in content data, store found percent symbol as is and advance to the next position + ++pTemplateStart; + } + if (paramName.length()) { + // call callback and replace with result. + // Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value. + // Data after pTemplateEnd may need to be moved. + // The first byte of data after placeholder is located at pTemplateEnd + 1. + // It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value). + const String paramValue(_callback(paramName)); + const char *pvstr = paramValue.c_str(); + const unsigned int pvlen = paramValue.length(); + const size_t numBytesCopied = std::min(pvlen, static_cast<unsigned int>(&data[originalLen - 1] - pTemplateStart + 1)); + // make room for param value + // 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store + if ((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) { + _cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]); + // 2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied); + len = originalLen; // fix issue with truncated data, not sure if it has any side effects + } else if (pTemplateEnd + 1 != pTemplateStart + numBytesCopied) { + // 2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit. + // Move the entire data after the placeholder + memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1); + } + // 3. replace placeholder with actual value + memcpy(pTemplateStart, pvstr, numBytesCopied); + // If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer) + if (numBytesCopied < pvlen) { + _cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen); + } else if (pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text... + // there is some free room, fill it from cache + const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied; + const size_t totalFreeRoom = originalLen - len + roomFreed; + len += _readDataFromCacheOrContent(&data[len - roomFreed], totalFreeRoom) - roomFreed; + } else { // result is copied fully; it is longer than placeholder text + const size_t roomTaken = pTemplateStart + numBytesCopied - pTemplateEnd - 1; + len = std::min(len + roomTaken, originalLen); + } + } + } // while(pTemplateStart) + return len; +} + +/* + * File Response + * */ + +void AsyncFileResponse::_setContentTypeFromPath(const String &path) { +#if HAVE_EXTERN_GET_Content_Type_FUNCTION +#ifndef ESP8266 + extern const char *getContentType(const String &path); +#else + extern const __FlashStringHelper *getContentType(const String &path); +#endif + _contentType = getContentType(path); +#else + if (path.endsWith(T__html)) { + _contentType = T_text_html; + } else if (path.endsWith(T__htm)) { + _contentType = T_text_html; + } else if (path.endsWith(T__css)) { + _contentType = T_text_css; + } else if (path.endsWith(T__json)) { + _contentType = T_application_json; + } else if (path.endsWith(T__js)) { + _contentType = T_application_javascript; + } else if (path.endsWith(T__png)) { + _contentType = T_image_png; + } else if (path.endsWith(T__gif)) { + _contentType = T_image_gif; + } else if (path.endsWith(T__jpg)) { + _contentType = T_image_jpeg; + } else if (path.endsWith(T__ico)) { + _contentType = T_image_x_icon; + } else if (path.endsWith(T__svg)) { + _contentType = T_image_svg_xml; + } else if (path.endsWith(T__eot)) { + _contentType = T_font_eot; + } else if (path.endsWith(T__woff)) { + _contentType = T_font_woff; + } else if (path.endsWith(T__woff2)) { + _contentType = T_font_woff2; + } else if (path.endsWith(T__ttf)) { + _contentType = T_font_ttf; + } else if (path.endsWith(T__xml)) { + _contentType = T_text_xml; + } else if (path.endsWith(T__pdf)) { + _contentType = T_application_pdf; + } else if (path.endsWith(T__zip)) { + _contentType = T_application_zip; + } else if (path.endsWith(T__gz)) { + _contentType = T_application_x_gzip; + } else { + _contentType = T_text_plain; + } +#endif +} + +AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) + : AsyncAbstractResponse(callback) { + _code = 200; + _path = path; + + if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { + _path = _path + T__gz; + addHeader(T_Content_Encoding, T_gzip, false); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = fs.open(_path, fs::FileOpenMode::read); + _contentLength = _content.size(); + + if (strlen(contentType) == 0) { + _setContentTypeFromPath(path); + } else { + _contentType = contentType; + } + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26 + path.length() - filenameStart]; + char *filename = (char *)path.c_str() + filenameStart; + + if (download) { + // set filename and force download + snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); + } else { + // set filename and force rendering + snprintf_P(buf, sizeof(buf), PSTR("inline")); + } + addHeader(T_Content_Disposition, buf, false); +} + +AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) + : AsyncAbstractResponse(callback) { + _code = 200; + _path = path; + + if (!download && String(content.name()).endsWith(T__gz) && !path.endsWith(T__gz)) { + addHeader(T_Content_Encoding, T_gzip, false); + _callback = nullptr; // Unable to process gzipped templates + _sendContentLength = true; + _chunked = false; + } + + _content = content; + _contentLength = _content.size(); + + if (strlen(contentType) == 0) { + _setContentTypeFromPath(path); + } else { + _contentType = contentType; + } + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26 + path.length() - filenameStart]; + char *filename = (char *)path.c_str() + filenameStart; + + if (download) { + snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); + } else { + snprintf_P(buf, sizeof(buf), PSTR("inline")); + } + addHeader(T_Content_Disposition, buf, false); +} + +size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len) { + return _content.read(data, len); +} + +/* + * Stream Response + * */ + +AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { + _code = 200; + _content = &stream; + _contentLength = len; + _contentType = contentType; +} + +size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len) { + size_t available = _content->available(); + size_t outLen = (available > len) ? len : available; + size_t i; + for (i = 0; i < outLen; i++) { + data[i] = _content->read(); + } + return outLen; +} + +/* + * Callback Response + * */ + +AsyncCallbackResponse::AsyncCallbackResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback) + : AsyncAbstractResponse(templateCallback) { + _code = 200; + _content = callback; + _contentLength = len; + if (!len) { + _sendContentLength = false; + } + _contentType = contentType; + _filledLength = 0; +} + +size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len) { + size_t ret = _content(data, len, _filledLength); + if (ret != RESPONSE_TRY_AGAIN) { + _filledLength += ret; + } + return ret; +} + +/* + * Chunked Response + * */ + +AsyncChunkedResponse::AsyncChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback) + : AsyncAbstractResponse(processorCallback) { + _code = 200; + _content = callback; + _contentLength = 0; + _contentType = contentType; + _sendContentLength = false; + _chunked = true; + _filledLength = 0; +} + +size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len) { + size_t ret = _content(data, len, _filledLength); + if (ret != RESPONSE_TRY_AGAIN) { + _filledLength += ret; + } + return ret; +} + +/* + * Progmem Response + * */ + +AsyncProgmemResponse::AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback) + : AsyncAbstractResponse(callback) { + _code = code; + _content = content; + _contentType = contentType; + _contentLength = len; + _readLength = 0; +} + +size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len) { + size_t left = _contentLength - _readLength; + if (left > len) { + memcpy_P(data, _content + _readLength, len); + _readLength += len; + return len; + } + memcpy_P(data, _content + _readLength, left); + _readLength += left; + return left; +} + +/* + * Response Stream (You can print/write/printf to it, up to the contentLen bytes) + * */ + +AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferSize) { + _code = 200; + _contentLength = 0; + _contentType = contentType; + // internal buffer will be null on allocation failure + _content = std::unique_ptr<cbuf>(new cbuf(bufferSize)); + if (bufferSize && _content->size() < bufferSize) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + } +} + +size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) { + return _content->read((char *)buf, maxLen); +} + +size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { + if (_started()) { + return 0; + } + if (len > _content->room()) { + size_t needed = len - _content->room(); + _content->resizeAdd(needed); + // log a warning if allocation failed, but do not return: keep writing the bytes we can + // with _content->write: if len is more than the available size in the buffer, only + // the available size will be written + if (len > _content->room()) { +#ifdef ESP32 + log_e("Failed to allocate"); +#endif + } + } + size_t written = _content->write((const char *)data, len); + _contentLength += written; + return written; +} + +size_t AsyncResponseStream::write(uint8_t data) { + return write(&data, 1); +} diff --git a/libraries/ESP_Async_WebServer/src/WebServer.cpp b/libraries/ESP_Async_WebServer/src/WebServer.cpp new file mode 100644 index 0000000..7fc54bf --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/WebServer.cpp @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include "ESPAsyncWebServer.h" +#include "WebHandlerImpl.h" + +using namespace asyncsrv; + +bool ON_STA_FILTER(AsyncWebServerRequest *request) { +#ifndef CONFIG_IDF_TARGET_ESP32H2 + return WiFi.localIP() == request->client()->localIP(); +#else + return false; +#endif +} + +bool ON_AP_FILTER(AsyncWebServerRequest *request) { +#ifndef CONFIG_IDF_TARGET_ESP32H2 + return WiFi.localIP() != request->client()->localIP(); +#else + return false; +#endif +} + +#ifndef HAVE_FS_FILE_OPEN_MODE +const char *fs::FileOpenMode::read = "r"; +const char *fs::FileOpenMode::write = "w"; +const char *fs::FileOpenMode::append = "a"; +#endif + +AsyncWebServer::AsyncWebServer(uint16_t port) : _server(port) { + _catchAllHandler = new AsyncCallbackWebHandler(); + _server.onClient( + [](void *s, AsyncClient *c) { + if (c == NULL) { + return; + } + c->setRxTimeout(3); + AsyncWebServerRequest *r = new AsyncWebServerRequest((AsyncWebServer *)s, c); + if (r == NULL) { + c->abort(); + delete c; + } + }, + this + ); +} + +AsyncWebServer::~AsyncWebServer() { + reset(); + end(); + delete _catchAllHandler; + _catchAllHandler = nullptr; // Prevent potential use-after-free +} + +AsyncWebRewrite &AsyncWebServer::addRewrite(std::shared_ptr<AsyncWebRewrite> rewrite) { + _rewrites.emplace_back(rewrite); + return *_rewrites.back().get(); +} + +AsyncWebRewrite &AsyncWebServer::addRewrite(AsyncWebRewrite *rewrite) { + _rewrites.emplace_back(rewrite); + return *_rewrites.back().get(); +} + +bool AsyncWebServer::removeRewrite(AsyncWebRewrite *rewrite) { + return removeRewrite(rewrite->from().c_str(), rewrite->toUrl().c_str()); +} + +bool AsyncWebServer::removeRewrite(const char *from, const char *to) { + for (auto r = _rewrites.begin(); r != _rewrites.end(); ++r) { + if (r->get()->from() == from && r->get()->toUrl() == to) { + _rewrites.erase(r); + return true; + } + } + return false; +} + +AsyncWebRewrite &AsyncWebServer::rewrite(const char *from, const char *to) { + _rewrites.emplace_back(std::make_shared<AsyncWebRewrite>(from, to)); + return *_rewrites.back().get(); +} + +AsyncWebHandler &AsyncWebServer::addHandler(AsyncWebHandler *handler) { + _handlers.emplace_back(handler); + return *(_handlers.back().get()); +} + +bool AsyncWebServer::removeHandler(AsyncWebHandler *handler) { + for (auto i = _handlers.begin(); i != _handlers.end(); ++i) { + if (i->get() == handler) { + _handlers.erase(i); + return true; + } + } + return false; +} + +void AsyncWebServer::begin() { + _server.setNoDelay(true); + _server.begin(); +} + +void AsyncWebServer::end() { + _server.end(); +} + +#if ASYNC_TCP_SSL_ENABLED +void AsyncWebServer::onSslFileRequest(AcSSlFileHandler cb, void *arg) { + _server.onSslFileRequest(cb, arg); +} + +void AsyncWebServer::beginSecure(const char *cert, const char *key, const char *password) { + _server.beginSecure(cert, key, password); +} +#endif + +void AsyncWebServer::_handleDisconnect(AsyncWebServerRequest *request) { + delete request; +} + +void AsyncWebServer::_rewriteRequest(AsyncWebServerRequest *request) { + // the last rewrite that matches the request will be used + // we do not break the loop to allow for multiple rewrites to be applied and only the last one to be used (allows overriding) + for (const auto &r : _rewrites) { + if (r->match(request)) { + request->_url = r->toUrl(); + request->_addGetParams(r->params()); + } + } +} + +void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request) { + for (auto &h : _handlers) { + if (h->filter(request) && h->canHandle(request)) { + request->setHandler(h.get()); + return; + } + } + // ESP_LOGD("AsyncWebServer", "No handler found for %s, using _catchAllHandler pointer: %p", request->url().c_str(), _catchAllHandler); + request->setHandler(_catchAllHandler); +} + +AsyncCallbackWebHandler &AsyncWebServer::on( + const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody +) { + AsyncCallbackWebHandler *handler = new AsyncCallbackWebHandler(); + handler->setUri(uri); + handler->setMethod(method); + handler->onRequest(onRequest); + handler->onUpload(onUpload); + handler->onBody(onBody); + 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); + return *handler; +} + +void AsyncWebServer::onNotFound(ArRequestHandlerFunction fn) { + _catchAllHandler->onRequest(fn); +} + +void AsyncWebServer::onFileUpload(ArUploadHandlerFunction fn) { + _catchAllHandler->onUpload(fn); +} + +void AsyncWebServer::onRequestBody(ArBodyHandlerFunction fn) { + _catchAllHandler->onBody(fn); +} + +AsyncWebHandler &AsyncWebServer::catchAllHandler() const { + return *_catchAllHandler; +} + +void AsyncWebServer::reset() { + _rewrites.clear(); + _handlers.clear(); + + _catchAllHandler->onRequest(NULL); + _catchAllHandler->onUpload(NULL); + _catchAllHandler->onBody(NULL); +} diff --git a/libraries/ESP_Async_WebServer/src/literals.h b/libraries/ESP_Async_WebServer/src/literals.h new file mode 100644 index 0000000..a69f78b --- /dev/null +++ b/libraries/ESP_Async_WebServer/src/literals.h @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#pragma once + +namespace asyncsrv { + +static constexpr const char *empty = ""; + +static constexpr const char *T__opaque = "\", opaque=\""; +static constexpr const char *T_100_CONTINUE = "100-continue"; +static constexpr const char *T_13 = "13"; +static constexpr const char *T_ACCEPT = "accept"; +static constexpr const char *T_Accept_Ranges = "accept-ranges"; +static constexpr const char *T_app_xform_urlencoded = "application/x-www-form-urlencoded"; +static constexpr const char *T_AUTH = "authorization"; +static constexpr const char *T_auth_nonce = "\", qop=\"auth\", nonce=\""; +static constexpr const char *T_BASIC = "basic"; +static constexpr const char *T_BASIC_REALM = "basic realm=\""; +static constexpr const char *T_BEARER = "bearer"; +static constexpr const char *T_BODY = "body"; +static constexpr const char *T_Cache_Control = "cache-control"; +static constexpr const char *T_chunked = "chunked"; +static constexpr const char *T_close = "close"; +static constexpr const char *T_cnonce = "cnonce"; +static constexpr const char *T_Connection = "connection"; +static constexpr const char *T_Content_Disposition = "content-disposition"; +static constexpr const char *T_Content_Encoding = "content-encoding"; +static constexpr const char *T_Content_Length = "content-length"; +static constexpr const char *T_Content_Type = "content-type"; +static constexpr const char *T_Content_Location = "content-location"; +static constexpr const char *T_Cookie = "cookie"; +static constexpr const char *T_CORS_ACAC = "access-control-allow-credentials"; +static constexpr const char *T_CORS_ACAH = "access-control-allow-headers"; +static constexpr const char *T_CORS_ACAM = "access-control-allow-methods"; +static constexpr const char *T_CORS_ACAO = "access-control-allow-origin"; +static constexpr const char *T_CORS_ACMA = "access-control-max-age"; +static constexpr const char *T_CORS_O = "origin"; +static constexpr const char *T_data_ = "data: "; +static constexpr const char *T_Date = "date"; +static constexpr const char *T_DIGEST = "digest"; +static constexpr const char *T_DIGEST_ = "digest "; +static constexpr const char *T_ETag = "etag"; +static constexpr const char *T_event_ = "event: "; +static constexpr const char *T_EXPECT = "expect"; +static constexpr const char *T_FALSE = "false"; +static constexpr const char *T_filename = "filename"; +static constexpr const char *T_gzip = "gzip"; +static constexpr const char *T_Host = "host"; +static constexpr const char *T_HTTP_1_0 = "HTTP/1.0"; +static constexpr const char *T_HTTP_100_CONT = "HTTP/1.1 100 Continue\r\n\r\n"; +static constexpr const char *T_id__ = "id: "; +static constexpr const char *T_IMS = "if-modified-since"; +static constexpr const char *T_INM = "if-none-match"; +static constexpr const char *T_keep_alive = "keep-alive"; +static constexpr const char *T_Last_Event_ID = "last-event-id"; +static constexpr const char *T_Last_Modified = "last-modified"; +static constexpr const char *T_LOCATION = "location"; +static constexpr const char *T_LOGIN_REQ = "Login Required"; +static constexpr const char *T_MULTIPART_ = "multipart/"; +static constexpr const char *T_name = "name"; +static constexpr const char *T_nc = "nc"; +static constexpr const char *T_no_cache = "no-cache"; +static constexpr const char *T_nonce = "nonce"; +static constexpr const char *T_none = "none"; +static constexpr const char *T_opaque = "opaque"; +static constexpr const char *T_qop = "qop"; +static constexpr const char *T_realm = "realm"; +static constexpr const char *T_realm__ = "realm=\""; +static constexpr const char *T_response = "response"; +static constexpr const char *T_retry_ = "retry: "; +static constexpr const char *T_retry_after = "retry-after"; +static constexpr const char *T_nn = "\n\n"; +static constexpr const char *T_rn = "\r\n"; +static constexpr const char *T_rnrn = "\r\n\r\n"; +static constexpr const char *T_Server = "server"; +static constexpr const char *T_Transfer_Encoding = "transfer-encoding"; +static constexpr const char *T_TRUE = "true"; +static constexpr const char *T_UPGRADE = "upgrade"; +static constexpr const char *T_uri = "uri"; +static constexpr const char *T_username = "username"; +static constexpr const char *T_WS = "websocket"; +static constexpr const char *T_WWW_AUTH = "www-authenticate"; + +// HTTP Methods + +static constexpr const char *T_ANY = "ANY"; +static constexpr const char *T_GET = "GET"; +static constexpr const char *T_POST = "POST"; +static constexpr const char *T_PUT = "PUT"; +static constexpr const char *T_DELETE = "DELETE"; +static constexpr const char *T_PATCH = "PATCH"; +static constexpr const char *T_HEAD = "HEAD"; +static constexpr const char *T_OPTIONS = "OPTIONS"; +static constexpr const char *T_UNKNOWN = "UNKNOWN"; + +// Req content types +static constexpr const char *T_RCT_NOT_USED = "RCT_NOT_USED"; +static constexpr const char *T_RCT_DEFAULT = "RCT_DEFAULT"; +static constexpr const char *T_RCT_HTTP = "RCT_HTTP"; +static constexpr const char *T_RCT_WS = "RCT_WS"; +static constexpr const char *T_RCT_EVENT = "RCT_EVENT"; +static constexpr const char *T_ERROR = "ERROR"; + +// extensions & MIME-Types +static constexpr const char *T__css = ".css"; +static constexpr const char *T__eot = ".eot"; +static constexpr const char *T__gif = ".gif"; +static constexpr const char *T__gz = ".gz"; +static constexpr const char *T__htm = ".htm"; +static constexpr const char *T__html = ".html"; +static constexpr const char *T__ico = ".ico"; +static constexpr const char *T__jpg = ".jpg"; +static constexpr const char *T__js = ".js"; +static constexpr const char *T__json = ".json"; +static constexpr const char *T__pdf = ".pdf"; +static constexpr const char *T__png = ".png"; +static constexpr const char *T__svg = ".svg"; +static constexpr const char *T__ttf = ".ttf"; +static constexpr const char *T__woff = ".woff"; +static constexpr const char *T__woff2 = ".woff2"; +static constexpr const char *T__xml = ".xml"; +static constexpr const char *T__zip = ".zip"; +static constexpr const char *T_application_javascript = "application/javascript"; +static constexpr const char *T_application_json = "application/json"; +static constexpr const char *T_application_msgpack = "application/msgpack"; +static constexpr const char *T_application_pdf = "application/pdf"; +static constexpr const char *T_application_x_gzip = "application/x-gzip"; +static constexpr const char *T_application_zip = "application/zip"; +static constexpr const char *T_font_eot = "font/eot"; +static constexpr const char *T_font_ttf = "font/ttf"; +static constexpr const char *T_font_woff = "font/woff"; +static constexpr const char *T_font_woff2 = "font/woff2"; +static constexpr const char *T_image_gif = "image/gif"; +static constexpr const char *T_image_jpeg = "image/jpeg"; +static constexpr const char *T_image_png = "image/png"; +static constexpr const char *T_image_svg_xml = "image/svg+xml"; +static constexpr const char *T_image_x_icon = "image/x-icon"; +static constexpr const char *T_text_css = "text/css"; +static constexpr const char *T_text_event_stream = "text/event-stream"; +static constexpr const char *T_text_html = "text/html"; +static constexpr const char *T_text_plain = "text/plain"; +static constexpr const char *T_text_xml = "text/xml"; + +// Response codes +static constexpr const char *T_HTTP_CODE_100 = "Continue"; +static constexpr const char *T_HTTP_CODE_101 = "Switching Protocols"; +static constexpr const char *T_HTTP_CODE_200 = "OK"; +static constexpr const char *T_HTTP_CODE_201 = "Created"; +static constexpr const char *T_HTTP_CODE_202 = "Accepted"; +static constexpr const char *T_HTTP_CODE_203 = "Non-Authoritative Information"; +static constexpr const char *T_HTTP_CODE_204 = "No Content"; +static constexpr const char *T_HTTP_CODE_205 = "Reset Content"; +static constexpr const char *T_HTTP_CODE_206 = "Partial Content"; +static constexpr const char *T_HTTP_CODE_300 = "Multiple Choices"; +static constexpr const char *T_HTTP_CODE_301 = "Moved Permanently"; +static constexpr const char *T_HTTP_CODE_302 = "Found"; +static constexpr const char *T_HTTP_CODE_303 = "See Other"; +static constexpr const char *T_HTTP_CODE_304 = "Not Modified"; +static constexpr const char *T_HTTP_CODE_305 = "Use Proxy"; +static constexpr const char *T_HTTP_CODE_307 = "Temporary Redirect"; +static constexpr const char *T_HTTP_CODE_400 = "Bad Request"; +static constexpr const char *T_HTTP_CODE_401 = "Unauthorized"; +static constexpr const char *T_HTTP_CODE_402 = "Payment Required"; +static constexpr const char *T_HTTP_CODE_403 = "Forbidden"; +static constexpr const char *T_HTTP_CODE_404 = "Not Found"; +static constexpr const char *T_HTTP_CODE_405 = "Method Not Allowed"; +static constexpr const char *T_HTTP_CODE_406 = "Not Acceptable"; +static constexpr const char *T_HTTP_CODE_407 = "Proxy Authentication Required"; +static constexpr const char *T_HTTP_CODE_408 = "Request Time-out"; +static constexpr const char *T_HTTP_CODE_409 = "Conflict"; +static constexpr const char *T_HTTP_CODE_410 = "Gone"; +static constexpr const char *T_HTTP_CODE_411 = "Length Required"; +static constexpr const char *T_HTTP_CODE_412 = "Precondition Failed"; +static constexpr const char *T_HTTP_CODE_413 = "Request Entity Too Large"; +static constexpr const char *T_HTTP_CODE_414 = "Request-URI Too Large"; +static constexpr const char *T_HTTP_CODE_415 = "Unsupported Media Type"; +static constexpr const char *T_HTTP_CODE_416 = "Requested range not satisfiable"; +static constexpr const char *T_HTTP_CODE_417 = "Expectation Failed"; +static constexpr const char *T_HTTP_CODE_429 = "Too Many Requests"; +static constexpr const char *T_HTTP_CODE_500 = "Internal Server Error"; +static constexpr const char *T_HTTP_CODE_501 = "Not Implemented"; +static constexpr const char *T_HTTP_CODE_502 = "Bad Gateway"; +static constexpr const char *T_HTTP_CODE_503 = "Service Unavailable"; +static constexpr const char *T_HTTP_CODE_504 = "Gateway Time-out"; +static constexpr const char *T_HTTP_CODE_505 = "HTTP Version not supported"; +static constexpr const char *T_HTTP_CODE_ANY = "Unknown code"; + +static constexpr const uint8_t T_only_once_headers_len = 11; +static constexpr const char *T_only_once_headers[] = {T_Content_Length, T_Content_Type, T_Date, T_ETag, T_Last_Modified, T_LOCATION, T_retry_after, + T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH}; + +} // namespace asyncsrv |
