chore: import local project into Gitea
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "AsyncEventSource.h"
|
||||
#include "AsyncWebServerLogging.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)) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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) {
|
||||
async_ws_log_e("Event message queue overflow: discard message");
|
||||
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) {
|
||||
async_ws_log_e("Event message queue overflow: discard message");
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// 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>
|
||||
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#ifdef LIBRETINY
|
||||
#ifdef round
|
||||
#undef round
|
||||
#endif
|
||||
#endif
|
||||
#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_ */
|
||||
@@ -0,0 +1,188 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "AsyncJson.h"
|
||||
#include "AsyncWebServerLogging.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) {
|
||||
// GET request:
|
||||
if (request->method() == HTTP_GET) {
|
||||
JsonVariant json;
|
||||
_onRequest(request, json);
|
||||
return;
|
||||
}
|
||||
|
||||
// POST / PUT / ... requests:
|
||||
// check if JSON body is too large, if it is, don't deserialize
|
||||
if (request->contentLength() > _maxContentLength) {
|
||||
async_ws_log_e("Content length exceeds maximum allowed");
|
||||
request->send(413);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->_tempObject == NULL) {
|
||||
// there is no body
|
||||
request->send(400);
|
||||
return;
|
||||
}
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 5
|
||||
DynamicJsonBuffer jsonBuffer;
|
||||
JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
|
||||
if (json.success()) {
|
||||
#elif ARDUINOJSON_VERSION_MAJOR == 6
|
||||
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
|
||||
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#else
|
||||
JsonDocument jsonBuffer;
|
||||
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
|
||||
if (!error) {
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#endif
|
||||
|
||||
_onRequest(request, json);
|
||||
} else {
|
||||
// error parsing the body
|
||||
request->send(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
|
||||
if (_onRequest) {
|
||||
// ignore callback if size is larger than maxContentLength
|
||||
if (total > _maxContentLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
// this check allows request->_tempObject to be initialized from a middleware
|
||||
if (request->_tempObject == NULL) {
|
||||
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string
|
||||
if (request->_tempObject == NULL) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request->_tempObject != NULL) {
|
||||
uint8_t *buffer = (uint8_t *)request->_tempObject;
|
||||
memcpy(buffer + index, data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ASYNC_JSON_SUPPORT
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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;
|
||||
#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_
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "AsyncMessagePack.h"
|
||||
#include "AsyncWebServerLogging.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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (request->_tempObject != NULL) {
|
||||
memcpy((uint8_t *)(request->_tempObject) + index, data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ASYNC_MSG_PACK_SUPPORT
|
||||
@@ -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
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
const AsyncWebHeader AsyncWebHeader::parse(const char *data) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers
|
||||
// In HTTP/1.X, a header is a case-insensitive name followed by a colon, then optional whitespace which will be ignored, and finally by its value
|
||||
if (!data) {
|
||||
return AsyncWebHeader(); // nullptr
|
||||
}
|
||||
if (data[0] == '\0') {
|
||||
return AsyncWebHeader(); // empty string
|
||||
}
|
||||
if (strchr(data, '\n') || strchr(data, '\r')) {
|
||||
return AsyncWebHeader(); // Invalid header format
|
||||
}
|
||||
const char *colon = strchr(data, ':');
|
||||
if (!colon) {
|
||||
return AsyncWebHeader(); // separator not found
|
||||
}
|
||||
if (colon == data) {
|
||||
return AsyncWebHeader(); // Header name cannot be empty
|
||||
}
|
||||
const char *startOfValue = colon + 1; // Skip the colon
|
||||
// skip one optional whitespace after the colon
|
||||
if (*startOfValue == ' ') {
|
||||
startOfValue++;
|
||||
}
|
||||
String name;
|
||||
name.reserve(colon - data);
|
||||
name.concat(data, colon - data);
|
||||
return AsyncWebHeader(name, String(startOfValue));
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef ASYNCWEBSERVER_LOG_CUSTOM
|
||||
// The user must provide the following macros in AsyncWebServerLoggingCustom.h:
|
||||
// async_ws_log_e, async_ws_log_w, async_ws_log_i, async_ws_log_d, async_ws_log_v
|
||||
#include <AsyncWebServerLoggingCustom.h>
|
||||
|
||||
#elif defined(ASYNCWEBSERVER_LOG_DEBUG)
|
||||
// Local Debug logging
|
||||
#include <HardwareSerial.h>
|
||||
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
|
||||
#else
|
||||
// Framework-based logging
|
||||
|
||||
/**
|
||||
* LibreTiny specific configurations
|
||||
*/
|
||||
#if defined(LIBRETINY)
|
||||
#include <Arduino.h>
|
||||
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
|
||||
|
||||
/**
|
||||
* Raspberry Pi Pico specific configurations
|
||||
*/
|
||||
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <HardwareSerial.h>
|
||||
// define log levels
|
||||
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
|
||||
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
|
||||
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
|
||||
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
|
||||
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
|
||||
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
|
||||
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
|
||||
// set default log level
|
||||
#ifndef ASYNCWEBSERVER_LOG_LEVEL
|
||||
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
|
||||
#endif
|
||||
// error
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
|
||||
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_e(format, ...)
|
||||
#endif
|
||||
// warn
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
|
||||
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_w(format, ...)
|
||||
#endif
|
||||
// info
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
|
||||
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_i(format, ...)
|
||||
#endif
|
||||
// debug
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
|
||||
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_d(format, ...)
|
||||
#endif
|
||||
// verbose
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
|
||||
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_v(format, ...)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* ESP8266 specific configurations
|
||||
*/
|
||||
#elif defined(ESP8266)
|
||||
#include <ets_sys.h>
|
||||
// define log levels
|
||||
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
|
||||
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
|
||||
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
|
||||
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
|
||||
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
|
||||
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
|
||||
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
|
||||
// set default log level
|
||||
#ifndef ASYNCWEBSERVER_LOG_LEVEL
|
||||
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
|
||||
#endif
|
||||
// error
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
|
||||
#define async_ws_log_e(format, ...) ets_printf(String(F("E async_ws %s() %d: " format)).c_str(), __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_e(format, ...)
|
||||
#endif
|
||||
// warn
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
|
||||
#define async_ws_log_w(format, ...) ets_printf(String(F("W async_ws %s() %d: " format)).c_str(), __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_w(format, ...)
|
||||
#endif
|
||||
// info
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
|
||||
#define async_ws_log_i(format, ...) ets_printf(String(F("I async_ws %s() %d: " format)).c_str(), __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_i(format, ...)
|
||||
#endif
|
||||
// debug
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
|
||||
#define async_ws_log_d(format, ...) ets_printf(String(F("D async_ws %s() %d: " format)).c_str(), __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_d(format, ...)
|
||||
#endif
|
||||
// verbose
|
||||
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
|
||||
#define async_ws_log_v(format, ...) ets_printf(String(F("V async_ws %s() %d: " format)).c_str(), __FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||
#else
|
||||
#define async_ws_log_v(format, ...)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Arduino specific configurations
|
||||
*/
|
||||
#elif defined(ARDUINO)
|
||||
#if defined(USE_ESP_IDF_LOG)
|
||||
#include <esp_log.h>
|
||||
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
|
||||
#else
|
||||
#include <esp32-hal-log.h>
|
||||
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
|
||||
#endif // USE_ESP_IDF_LOG
|
||||
|
||||
/**
|
||||
* ESP-IDF specific configurations
|
||||
*/
|
||||
#else
|
||||
#include <esp_log.h>
|
||||
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
|
||||
#endif // !LIBRETINY && !ARDUINO
|
||||
|
||||
#endif // ASYNCWEBSERVER_LOG_CUSTOM
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
/**
|
||||
* @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching.
|
||||
*
|
||||
* This method serves files over HTTP from the provided filesystem. If a compressed version of the file
|
||||
* (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file.
|
||||
* It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified`
|
||||
* if the client's `If-None-Match` header matches the generated ETag.
|
||||
*
|
||||
* @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.).
|
||||
* @param path Path to the file to be served.
|
||||
* @param contentType Optional MIME type of the file to be sent.
|
||||
* If contentType is "" it will be obtained from the file extension
|
||||
* @param download If true, forces the file to be sent as a download.
|
||||
* @param callback Optional template processor for dynamic content generation.
|
||||
* Templates will not be processed in compressed files.
|
||||
*
|
||||
* @note If neither the file nor its compressed version exists, responds with `404 Not Found`.
|
||||
*/
|
||||
void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) {
|
||||
// Check uncompressed file first
|
||||
if (fs.exists(path)) {
|
||||
send(beginResponse(fs, path, contentType, download, callback));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle compressed version
|
||||
const String gzPath = path + asyncsrv::T__gz;
|
||||
File gzFile = fs.open(gzPath, fs::FileOpenMode::read);
|
||||
|
||||
// ETag validation
|
||||
if (this->hasHeader(asyncsrv::T_INM)) {
|
||||
// Generate server ETag from CRC in gzip trailer
|
||||
char serverETag[9];
|
||||
if (!_getEtag(gzFile, serverETag)) {
|
||||
// Compressed file not found or invalid
|
||||
send(404);
|
||||
gzFile.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare with client's ETag
|
||||
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
|
||||
if (inmHeader && inmHeader->value() == serverETag) {
|
||||
gzFile.close();
|
||||
this->send(304); // Not Modified
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send compressed file response
|
||||
gzFile.close();
|
||||
send(beginResponse(fs, path, contentType, download, callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generates an ETag string from the CRC32 trailer of a GZIP file.
|
||||
*
|
||||
* This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file
|
||||
* and converts it into an 8-character hexadecimal ETag string (null-terminated).
|
||||
*
|
||||
* @param gzFile Opened file handle pointing to the GZIP file.
|
||||
* @param eTag Output buffer to store the generated ETag.
|
||||
* Must be pre-allocated with at least 9 bytes (8 for hex digits + 1 for null terminator).
|
||||
*
|
||||
* @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed).
|
||||
*/
|
||||
bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) {
|
||||
static constexpr char hexChars[] = "0123456789ABCDEF";
|
||||
|
||||
if (!gzFile.seek(gzFile.size() - 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t crc;
|
||||
gzFile.read(reinterpret_cast<uint8_t *>(&crc), sizeof(crc));
|
||||
|
||||
etag[0] = hexChars[(crc >> 4) & 0x0F];
|
||||
etag[1] = hexChars[crc & 0x0F];
|
||||
etag[2] = hexChars[(crc >> 12) & 0x0F];
|
||||
etag[3] = hexChars[(crc >> 8) & 0x0F];
|
||||
etag[4] = hexChars[(crc >> 20) & 0x0F];
|
||||
etag[5] = hexChars[(crc >> 16) & 0x0F];
|
||||
etag[6] = hexChars[(crc >> 28)];
|
||||
etag[7] = hexChars[(crc >> 24) & 0x0F];
|
||||
etag[8] = '\0';
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -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 8
|
||||
/** Patch version number (x.x.X) */
|
||||
#define ASYNCWEBSERVER_VERSION_PATCH 1
|
||||
|
||||
/**
|
||||
* 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,559 @@
|
||||
// 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>
|
||||
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#ifdef LIBRETINY
|
||||
#ifdef round
|
||||
#undef round
|
||||
#endif
|
||||
#endif
|
||||
#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 <AsyncWebServerLogging.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;
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
_len = 0;
|
||||
} else {
|
||||
memcpy(_data, data, len);
|
||||
}
|
||||
} else {
|
||||
_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
~AsyncWebSocketControl() {
|
||||
if (_data != NULL) {
|
||||
free(_data);
|
||||
}
|
||||
}
|
||||
|
||||
bool finished() const {
|
||||
return _finished;
|
||||
}
|
||||
uint8_t opcode() {
|
||||
return _opcode;
|
||||
}
|
||||
uint8_t len() {
|
||||
return _len + 2;
|
||||
}
|
||||
size_t send(AsyncClient *client);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
/** Message type as defined by enum AwsFrameType.
|
||||
* 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_ */
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
// 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 "));
|
||||
#ifndef LIBRETINY
|
||||
_out->print(request->client()->remoteIP().toString());
|
||||
#else
|
||||
_out->print(request->client()->remoteIP());
|
||||
#endif
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "WebAuthentication.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
#include <libb64/cencode.h>
|
||||
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
|
||||
#include <MD5Builder.h>
|
||||
#else
|
||||
#include "md5.h"
|
||||
#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)) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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)) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
return emptyString;
|
||||
}
|
||||
|
||||
String in;
|
||||
if (!in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2)) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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)) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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"
|
||||
#include "AsyncWebServerLogging.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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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());
|
||||
#elif defined(LIBRETINY)
|
||||
long val = lw ^ request->_tempFile.size();
|
||||
etag = String(val);
|
||||
#else
|
||||
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
|
||||
#endif
|
||||
} else {
|
||||
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
|
||||
etag = String(request->_tempFile.size());
|
||||
#else
|
||||
etag = request->_tempFile.size();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool not_modified = false;
|
||||
|
||||
// if-none-match has precedence over if-modified-since
|
||||
if (request->hasHeader(T_INM)) {
|
||||
not_modified = request->header(T_INM).equals(etag);
|
||||
} else if (_last_modified.length()) {
|
||||
not_modified = request->header(T_IMS).equals(_last_modified);
|
||||
}
|
||||
|
||||
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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
|
||||
response->addHeader(T_ETag, etag.c_str());
|
||||
|
||||
if (_last_modified.length()) {
|
||||
response->addHeader(T_Last_Modified, _last_modified.c_str());
|
||||
}
|
||||
if (_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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
// 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;
|
||||
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_ */
|
||||
@@ -0,0 +1,925 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebResponseImpl.h"
|
||||
#include "AsyncWebServerLogging.h"
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
/*
|
||||
* 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(AsyncWebHeader &&header, bool replaceExisting) {
|
||||
if (!header) {
|
||||
return false; // invalid header
|
||||
}
|
||||
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
|
||||
if (i->name().equalsIgnoreCase(header.name())) {
|
||||
// header already set
|
||||
if (replaceExisting) {
|
||||
// remove, break and add the new one
|
||||
_headers.erase(i);
|
||||
break;
|
||||
} else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name
|
||||
// do not update
|
||||
return false;
|
||||
} else {
|
||||
break; // accept multiple headers with the same name
|
||||
}
|
||||
}
|
||||
}
|
||||
// header was not found found, or existing one was removed
|
||||
_headers.emplace_back(std::move(header));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) {
|
||||
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
|
||||
if (i->name().equalsIgnoreCase(name)) {
|
||||
// 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) {
|
||||
async_ws_log_d("(chunk) out of in-flight credits");
|
||||
}
|
||||
|
||||
_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) {
|
||||
// async_ws_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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
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
|
||||
* */
|
||||
|
||||
/**
|
||||
* @brief Sets the content type based on the file path extension
|
||||
*
|
||||
* This method determines the appropriate MIME content type for a file based on its
|
||||
* file extension. It supports both external content type functions (if available)
|
||||
* and an internal mapping of common file extensions to their corresponding MIME types.
|
||||
*
|
||||
* @param path The file path string from which to extract the extension
|
||||
* @note The method modifies the internal _contentType member variable
|
||||
*/
|
||||
void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
|
||||
#if HAVE_EXTERN_GET_Content_Type_FUNCTION
|
||||
#ifndef ESP8266
|
||||
extern const char *getContentType(const String &path);
|
||||
#else
|
||||
extern const __FlashStringHelper *getContentType(const String &path);
|
||||
#endif
|
||||
_contentType = getContentType(path);
|
||||
#else
|
||||
const char *cpath = path.c_str();
|
||||
const char *dot = strrchr(cpath, '.');
|
||||
|
||||
if (!dot) {
|
||||
_contentType = T_application_octet_stream;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(dot, T__html) == 0 || strcmp(dot, T__htm) == 0) {
|
||||
_contentType = T_text_html;
|
||||
} else if (strcmp(dot, T__css) == 0) {
|
||||
_contentType = T_text_css;
|
||||
} else if (strcmp(dot, T__js) == 0 || strcmp(dot, T__mjs) == 0) {
|
||||
_contentType = T_text_javascript;
|
||||
} else if (strcmp(dot, T__json) == 0) {
|
||||
_contentType = T_application_json;
|
||||
} else if (strcmp(dot, T__png) == 0) {
|
||||
_contentType = T_image_png;
|
||||
} else if (strcmp(dot, T__ico) == 0) {
|
||||
_contentType = T_image_x_icon;
|
||||
} else if (strcmp(dot, T__svg) == 0) {
|
||||
_contentType = T_image_svg_xml;
|
||||
} else if (strcmp(dot, T__jpg) == 0) {
|
||||
_contentType = T_image_jpeg;
|
||||
} else if (strcmp(dot, T__webp) == 0) {
|
||||
_contentType = T_image_webp;
|
||||
} else if (strcmp(dot, T__avif) == 0) {
|
||||
_contentType = T_image_avif;
|
||||
} else if (strcmp(dot, T__gif) == 0) {
|
||||
_contentType = T_image_gif;
|
||||
} else if (strcmp(dot, T__woff2) == 0) {
|
||||
_contentType = T_font_woff2;
|
||||
} else if (strcmp(dot, T__woff) == 0) {
|
||||
_contentType = T_font_woff;
|
||||
} else if (strcmp(dot, T__ttf) == 0) {
|
||||
_contentType = T_font_ttf;
|
||||
} else if (strcmp(dot, T__xml) == 0) {
|
||||
_contentType = T_text_xml;
|
||||
} else if (strcmp(dot, T__pdf) == 0) {
|
||||
_contentType = T_application_pdf;
|
||||
} else if (strcmp(dot, T__mp4) == 0) {
|
||||
_contentType = T_video_mp4;
|
||||
} else if (strcmp(dot, T__opus) == 0) {
|
||||
_contentType = T_audio_opus;
|
||||
} else if (strcmp(dot, T__webm) == 0) {
|
||||
_contentType = T_video_webm;
|
||||
} else if (strcmp(dot, T__txt) == 0) {
|
||||
_contentType = T_text_plain;
|
||||
} else {
|
||||
_contentType = T_application_octet_stream;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Constructor for AsyncFileResponse that handles file serving with compression support
|
||||
*
|
||||
* This constructor creates an AsyncFileResponse object that can serve files from a filesystem,
|
||||
* with automatic fallback to gzip-compressed versions if the original file is not found.
|
||||
* It also handles ETag generation for caching and supports both inline and download modes.
|
||||
*
|
||||
* @param fs Reference to the filesystem object used to open files
|
||||
* @param path Path to the file to be served (without compression extension)
|
||||
* @param contentType MIME type of the file content (empty string for auto-detection)
|
||||
* @param download If true, file will be served as download attachment; if false, as inline content
|
||||
* @param callback Template processor callback for dynamic content processing
|
||||
*/
|
||||
AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
|
||||
: AsyncAbstractResponse(callback) {
|
||||
|
||||
// Try to open the uncompressed version first
|
||||
_content = fs.open(path, fs::FileOpenMode::read);
|
||||
if (!_content.available()) {
|
||||
// If not available try to open the compressed version (.gz)
|
||||
String gzPath;
|
||||
uint16_t pathLen = path.length();
|
||||
gzPath.reserve(pathLen + 3);
|
||||
gzPath.concat(path);
|
||||
gzPath.concat(asyncsrv::T__gz);
|
||||
_content = fs.open(gzPath, fs::FileOpenMode::read);
|
||||
|
||||
char serverETag[9];
|
||||
if (AsyncWebServerRequest::_getEtag(_content, serverETag)) {
|
||||
addHeader(T_Content_Encoding, T_gzip, false);
|
||||
_callback = nullptr; // Unable to process zipped templates
|
||||
_sendContentLength = true;
|
||||
_chunked = false;
|
||||
|
||||
// Add ETag and cache headers
|
||||
addHeader(T_ETag, serverETag, true);
|
||||
addHeader(T_Cache_Control, T_no_cache, true);
|
||||
|
||||
_content.seek(0);
|
||||
} else {
|
||||
// File is corrupted or invalid
|
||||
_code = 404;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_contentLength = _content.size();
|
||||
|
||||
if (*contentType == '\0') {
|
||||
_setContentTypeFromPath(path);
|
||||
} else {
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
if (download) {
|
||||
// Extract filename from path and set as download attachment
|
||||
int filenameStart = path.lastIndexOf('/') + 1;
|
||||
const char *filename = path.c_str() + filenameStart;
|
||||
String buf;
|
||||
buf.reserve(strlen(T_attachment) + strlen(filename) + 2);
|
||||
buf = T_attachment;
|
||||
buf += filename;
|
||||
buf += "\"";
|
||||
addHeader(T_Content_Disposition, buf, false);
|
||||
} else {
|
||||
// Serve file inline (display in browser)
|
||||
addHeader(T_Content_Disposition, T_inline, false);
|
||||
}
|
||||
|
||||
_code = 200;
|
||||
}
|
||||
|
||||
AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
|
||||
: AsyncAbstractResponse(callback) {
|
||||
_code = 200;
|
||||
|
||||
if (String(content.name()).endsWith(T__gz) && !path.endsWith(T__gz)) {
|
||||
addHeader(T_Content_Encoding, T_gzip, false);
|
||||
_callback = nullptr; // Unable to process gzipped templates
|
||||
_sendContentLength = true;
|
||||
_chunked = false;
|
||||
}
|
||||
|
||||
_content = content;
|
||||
_contentLength = _content.size();
|
||||
|
||||
if (*contentType == '\0') {
|
||||
_setContentTypeFromPath(path);
|
||||
} else {
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
if (download) {
|
||||
// Extract filename from path and set as download attachment
|
||||
int filenameStart = path.lastIndexOf('/') + 1;
|
||||
const char *filename = path.c_str() + filenameStart;
|
||||
String buf;
|
||||
buf.reserve(strlen(T_attachment) + strlen(filename) + 2);
|
||||
buf = T_attachment;
|
||||
buf += filename;
|
||||
buf += "\"";
|
||||
addHeader(T_Content_Disposition, buf, false);
|
||||
} else {
|
||||
// Serve file inline (display in browser)
|
||||
addHeader(T_Content_Disposition, T_inline, 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) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
async_ws_log_e("Failed to allocate");
|
||||
}
|
||||
}
|
||||
size_t written = _content->write((const char *)data, len);
|
||||
_contentLength += written;
|
||||
return written;
|
||||
}
|
||||
|
||||
size_t AsyncResponseStream::write(uint8_t data) {
|
||||
return write(&data, 1);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
|
||||
#include "ESPAsyncWebServer.h"
|
||||
#include "WebHandlerImpl.h"
|
||||
|
||||
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
|
||||
#include <WiFi.h>
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#else
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
using namespace asyncsrv;
|
||||
|
||||
bool ON_STA_FILTER(AsyncWebServerRequest *request) {
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
return WiFi.localIP() == request->client()->localIP();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ON_AP_FILTER(AsyncWebServerRequest *request) {
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// 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_attachment = "attachment; filename=\"";
|
||||
static constexpr const char *T_AUTH = "Authorization";
|
||||
static constexpr const char *T_auth_nonce = "\", qop=\"auth\", nonce=\"";
|
||||
static constexpr const char *T_BASIC = "Basic";
|
||||
static constexpr const char *T_BASIC_REALM = "Basic realm=\"";
|
||||
static constexpr const char *T_BEARER = "Bearer";
|
||||
static constexpr const char *T_BODY = "body";
|
||||
static constexpr const char *T_Cache_Control = "Cache-Control";
|
||||
static constexpr const char *T_chunked = "chunked";
|
||||
static constexpr const char *T_close = "close";
|
||||
static constexpr const char *T_cnonce = "cnonce";
|
||||
static constexpr const char *T_Connection = "Connection";
|
||||
static constexpr const char *T_Content_Disposition = "Content-Disposition";
|
||||
static constexpr const char *T_Content_Encoding = "Content-Encoding";
|
||||
static constexpr const char *T_Content_Length = "Content-Length";
|
||||
static constexpr const char *T_Content_Type = "Content-Type";
|
||||
static constexpr const char *T_Content_Location = "Content-Location";
|
||||
static constexpr const char *T_Cookie = "Cookie";
|
||||
static constexpr const char *T_CORS_ACAC = "Access-Control-Allow-Credentials";
|
||||
static constexpr const char *T_CORS_ACAH = "Access-Control-Allow-Headers";
|
||||
static constexpr const char *T_CORS_ACAM = "Access-Control-Allow-Methods";
|
||||
static constexpr const char *T_CORS_ACAO = "Access-Control-Allow-Origin";
|
||||
static constexpr const char *T_CORS_ACMA = "Access-Control-Max-Age";
|
||||
static constexpr const char *T_CORS_O = "Origin";
|
||||
static constexpr const char *T_data_ = "data: ";
|
||||
static constexpr const char *T_Date = "Date";
|
||||
static constexpr const char *T_DIGEST = "Digest";
|
||||
static constexpr const char *T_DIGEST_ = "Digest ";
|
||||
static constexpr const char *T_ETag = "ETag";
|
||||
static constexpr const char *T_event_ = "event: ";
|
||||
static constexpr const char *T_EXPECT = "Expect";
|
||||
static constexpr const char *T_FALSE = "false";
|
||||
static constexpr const char *T_filename = "filename";
|
||||
static constexpr const char *T_gzip = "gzip";
|
||||
static constexpr const char *T_Host = "host";
|
||||
static constexpr const char *T_HTTP_1_0 = "HTTP/1.0";
|
||||
static constexpr const char *T_HTTP_100_CONT = "HTTP/1.1 100 Continue\r\n\r\n";
|
||||
static constexpr const char *T_id__ = "id: ";
|
||||
static constexpr const char *T_IMS = "If-Modified-Since";
|
||||
static constexpr const char *T_INM = "If-None-Match";
|
||||
static constexpr const char *T_inline = "inline";
|
||||
static constexpr const char *T_keep_alive = "keep-alive";
|
||||
static constexpr const char *T_Last_Event_ID = "Last-Event-ID";
|
||||
static constexpr const char *T_Last_Modified = "Last-Modified";
|
||||
static constexpr const char *T_LOCATION = "Location";
|
||||
static constexpr const char *T_LOGIN_REQ = "Login Required";
|
||||
static constexpr const char *T_MULTIPART_ = "multipart/";
|
||||
static constexpr const char *T_name = "name";
|
||||
static constexpr const char *T_nc = "nc";
|
||||
static constexpr const char *T_no_cache = "no-cache";
|
||||
static constexpr const char *T_nonce = "nonce";
|
||||
static constexpr const char *T_none = "none";
|
||||
static constexpr const char *T_opaque = "opaque";
|
||||
static constexpr const char *T_qop = "qop";
|
||||
static constexpr const char *T_realm = "realm";
|
||||
static constexpr const char *T_realm__ = "realm=\"";
|
||||
static constexpr const char *T_response = "response";
|
||||
static constexpr const char *T_retry_ = "retry: ";
|
||||
static constexpr const char *T_retry_after = "Retry-After";
|
||||
static constexpr const char *T_nn = "\n\n";
|
||||
static constexpr const char *T_rn = "\r\n";
|
||||
static constexpr const char *T_rnrn = "\r\n\r\n";
|
||||
static constexpr const char *T_Server = "Server";
|
||||
static constexpr const char *T_Transfer_Encoding = "Transfer-Encoding";
|
||||
static constexpr const char *T_TRUE = "true";
|
||||
static constexpr const char *T_UPGRADE = "Upgrade";
|
||||
static constexpr const char *T_uri = "uri";
|
||||
static constexpr const char *T_username = "username";
|
||||
static constexpr const char *T_WS = "websocket";
|
||||
static constexpr const char *T_WWW_AUTH = "WWW-Authenticate";
|
||||
|
||||
// HTTP Methods
|
||||
static constexpr const char *T_ANY = "ANY";
|
||||
static constexpr const char *T_GET = "GET";
|
||||
static constexpr const char *T_POST = "POST";
|
||||
static constexpr const char *T_PUT = "PUT";
|
||||
static constexpr const char *T_DELETE = "DELETE";
|
||||
static constexpr const char *T_PATCH = "PATCH";
|
||||
static constexpr const char *T_HEAD = "HEAD";
|
||||
static constexpr const char *T_OPTIONS = "OPTIONS";
|
||||
static constexpr const char *T_UNKNOWN = "UNKNOWN";
|
||||
|
||||
// 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__avif = ".avif"; // AVIF: Highly compressed images. Compatible with all modern browsers.
|
||||
static constexpr const char *T__csv = ".csv"; // CSV: Data logging and configuration
|
||||
static constexpr const char *T__css = ".css"; // CSS: Styling for web interfaces
|
||||
static constexpr const char *T__gif = ".gif"; // GIF: Simple animations. Legacy support
|
||||
static constexpr const char *T__gz = ".gz"; // GZ: compressed files
|
||||
static constexpr const char *T__htm = ".htm"; // HTM: Web interface files
|
||||
static constexpr const char *T__html = ".html"; // HTML: Web interface files
|
||||
static constexpr const char *T__ico = ".ico"; // ICO: Favicons, system icons. Legacy support
|
||||
static constexpr const char *T__jpg = ".jpg"; // JPEG/JPG: Photos. Legacy support
|
||||
static constexpr const char *T__js = ".js"; // JavaScript: Interactive functionality
|
||||
static constexpr const char *T__json = ".json"; // JSON: Data exchange format
|
||||
static constexpr const char *T__mp4 = ".mp4"; // MP4: Proprietary format. Worse compression than WEBM.
|
||||
static constexpr const char *T__mjs = ".mjs"; // MJS: JavaScript module format
|
||||
static constexpr const char *T__opus = ".opus"; // OPUS: High compression audio format
|
||||
static constexpr const char *T__pdf = ".pdf"; // PDF: Universal document format
|
||||
static constexpr const char *T__png = ".png"; // PNG: Icons, logos, transparency. Legacy support
|
||||
static constexpr const char *T__svg = ".svg"; // SVG: Vector graphics, icons (scalable, tiny file sizes)
|
||||
static constexpr const char *T__ttf = ".ttf"; // TTF: Font file. Legacy support
|
||||
static constexpr const char *T__txt = ".txt"; // TXT: Plain text files
|
||||
static constexpr const char *T__webm = ".webm"; // WebM: Video. Open source, optimized for web. Compatible with all modern browsers.
|
||||
static constexpr const char *T__webp = ".webp"; // WebP: Highly compressed images. Compatible with all modern browsers.
|
||||
static constexpr const char *T__woff = ".woff"; // WOFF: Font file. Legacy support
|
||||
static constexpr const char *T__woff2 = ".woff2"; // WOFF2: Better compression. Compatible with all modern browsers.
|
||||
static constexpr const char *T__xml = ".xml"; // XML: Configuration and data files
|
||||
static constexpr const char *T_application_javascript = "application/javascript"; // Obsolete type for JavaScript
|
||||
static constexpr const char *T_application_json = "application/json";
|
||||
static constexpr const char *T_application_msgpack = "application/msgpack";
|
||||
static constexpr const char *T_application_octet_stream = "application/octet-stream";
|
||||
static constexpr const char *T_application_pdf = "application/pdf";
|
||||
static constexpr const char *T_app_xform_urlencoded = "application/x-www-form-urlencoded";
|
||||
static constexpr const char *T_audio_opus = "audio/opus";
|
||||
static constexpr const char *T_font_ttf = "font/ttf";
|
||||
static constexpr const char *T_font_woff = "font/woff";
|
||||
static constexpr const char *T_font_woff2 = "font/woff2";
|
||||
static constexpr const char *T_image_avif = "image/avif";
|
||||
static constexpr const char *T_image_gif = "image/gif";
|
||||
static constexpr const char *T_image_jpeg = "image/jpeg";
|
||||
static constexpr const char *T_image_png = "image/png";
|
||||
static constexpr const char *T_image_svg_xml = "image/svg+xml";
|
||||
static constexpr const char *T_image_webp = "image/webp";
|
||||
static constexpr const char *T_image_x_icon = "image/x-icon";
|
||||
static constexpr const char *T_text_css = "text/css";
|
||||
static constexpr const char *T_text_csv = "text/csv";
|
||||
static constexpr const char *T_text_event_stream = "text/event-stream";
|
||||
static constexpr const char *T_text_html = "text/html";
|
||||
static constexpr const char *T_text_javascript = "text/javascript";
|
||||
static constexpr const char *T_text_plain = "text/plain";
|
||||
static constexpr const char *T_text_xml = "text/xml";
|
||||
static constexpr const char *T_video_mp4 = "video/mp4";
|
||||
static constexpr const char *T_video_webm = "video/webm";
|
||||
|
||||
// Response codes
|
||||
static constexpr const char *T_HTTP_CODE_100 = "Continue";
|
||||
static constexpr const char *T_HTTP_CODE_101 = "Switching Protocols";
|
||||
static constexpr const char *T_HTTP_CODE_200 = "OK";
|
||||
static constexpr const char *T_HTTP_CODE_201 = "Created";
|
||||
static constexpr const char *T_HTTP_CODE_202 = "Accepted";
|
||||
static constexpr const char *T_HTTP_CODE_203 = "Non-Authoritative Information";
|
||||
static constexpr const char *T_HTTP_CODE_204 = "No Content";
|
||||
static constexpr const char *T_HTTP_CODE_205 = "Reset Content";
|
||||
static constexpr const char *T_HTTP_CODE_206 = "Partial Content";
|
||||
static constexpr const char *T_HTTP_CODE_300 = "Multiple Choices";
|
||||
static constexpr const char *T_HTTP_CODE_301 = "Moved Permanently";
|
||||
static constexpr const char *T_HTTP_CODE_302 = "Found";
|
||||
static constexpr const char *T_HTTP_CODE_303 = "See Other";
|
||||
static constexpr const char *T_HTTP_CODE_304 = "Not Modified";
|
||||
static constexpr const char *T_HTTP_CODE_305 = "Use Proxy";
|
||||
static constexpr const char *T_HTTP_CODE_307 = "Temporary Redirect";
|
||||
static constexpr const char *T_HTTP_CODE_400 = "Bad Request";
|
||||
static constexpr const char *T_HTTP_CODE_401 = "Unauthorized";
|
||||
static constexpr const char *T_HTTP_CODE_402 = "Payment Required";
|
||||
static constexpr const char *T_HTTP_CODE_403 = "Forbidden";
|
||||
static constexpr const char *T_HTTP_CODE_404 = "Not Found";
|
||||
static constexpr const char *T_HTTP_CODE_405 = "Method Not Allowed";
|
||||
static constexpr const char *T_HTTP_CODE_406 = "Not Acceptable";
|
||||
static constexpr const char *T_HTTP_CODE_407 = "Proxy Authentication Required";
|
||||
static constexpr const char *T_HTTP_CODE_408 = "Request Time-out";
|
||||
static constexpr const char *T_HTTP_CODE_409 = "Conflict";
|
||||
static constexpr const char *T_HTTP_CODE_410 = "Gone";
|
||||
static constexpr const char *T_HTTP_CODE_411 = "Length Required";
|
||||
static constexpr const char *T_HTTP_CODE_412 = "Precondition Failed";
|
||||
static constexpr const char *T_HTTP_CODE_413 = "Request Entity Too Large";
|
||||
static constexpr const char *T_HTTP_CODE_414 = "Request-URI Too Large";
|
||||
static constexpr const char *T_HTTP_CODE_415 = "Unsupported Media Type";
|
||||
static constexpr const char *T_HTTP_CODE_416 = "Requested Range Not Satisfiable";
|
||||
static constexpr const char *T_HTTP_CODE_417 = "Expectation Failed";
|
||||
static constexpr const char *T_HTTP_CODE_429 = "Too Many Requests";
|
||||
static constexpr const char *T_HTTP_CODE_500 = "Internal Server Error";
|
||||
static constexpr const char *T_HTTP_CODE_501 = "Not Implemented";
|
||||
static constexpr const char *T_HTTP_CODE_502 = "Bad Gateway";
|
||||
static constexpr const char *T_HTTP_CODE_503 = "Service Unavailable";
|
||||
static constexpr const char *T_HTTP_CODE_504 = "Gateway Time-out";
|
||||
static constexpr const char *T_HTTP_CODE_505 = "HTTP Version Not Supported";
|
||||
static constexpr const char *T_HTTP_CODE_ANY = "Unknown code";
|
||||
|
||||
static constexpr const char *T_only_once_headers[] = {
|
||||
T_Accept_Ranges, T_Content_Length, T_Content_Type, T_Connection, T_CORS_ACAC, T_CORS_ACAH, T_CORS_ACAM, T_CORS_ACAO,
|
||||
T_CORS_ACMA, T_CORS_O, T_Date, T_DIGEST, T_ETag, T_Last_Modified, T_LOCATION, T_retry_after,
|
||||
T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH
|
||||
};
|
||||
static constexpr size_t T_only_once_headers_len = sizeof(T_only_once_headers) / sizeof(T_only_once_headers[0]);
|
||||
|
||||
} // namespace asyncsrv
|
||||
Reference in New Issue
Block a user