Skip to content

Commit

Permalink
feat(webserver): Middleware with default middleware for cors, authc, …
Browse files Browse the repository at this point in the history
…curl-like logging
  • Loading branch information
mathieucarbou committed Dec 18, 2024
1 parent bd7a74e commit a3d10e9
Show file tree
Hide file tree
Showing 14 changed files with 897 additions and 101 deletions.
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ set(ARDUINO_LIBRARY_USB_SRCS
set(ARDUINO_LIBRARY_WebServer_SRCS
libraries/WebServer/src/WebServer.cpp
libraries/WebServer/src/Parsing.cpp
libraries/WebServer/src/detail/mimetable.cpp)
libraries/WebServer/src/detail/mimetable.cpp
libraries/WebServer/src/middleware/MiddlewareChain.cpp
libraries/WebServer/src/middleware/AuthenticationMiddleware.cpp
libraries/WebServer/src/middleware/CorsMiddleware.cpp
libraries/WebServer/src/middleware/LoggingMiddleware.cpp)

set(ARDUINO_LIBRARY_NetworkClientSecure_SRCS
libraries/NetworkClientSecure/src/ssl_client.cpp
Expand Down
186 changes: 186 additions & 0 deletions libraries/WebServer/examples/Middleware/Middleware.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Basic example of using Middlewares with WebServer
*
* Middleware are common request/response processing functions that can be applied globally to all incoming requests or to specific handlers.
* They allow for a common processing thus saving memory and space to avoid duplicating code or states on multiple handlers.
*
* Once the example is flashed (with the correct WiFi credentials), you can test the following scenarios with the listed curl commands:
* - CORS Middleware: answers to OPTIONS requests with the specified CORS headers and also add CORS headers to the response when the request has the Origin header
* - Logging Middleware: logs the request and response to an output in a curl-like format
* - Authentication Middleware: test the authentication with Digest Auth
*
* You can also add your own Middleware by extending the Middleware class and implementing the run method.
* When implementing a Middleware, you can decide when to call the next Middleware in the chain by calling next().
*
* Middleware are execute in order of addition, the ones attached to the server will be executed first.
*/
#include <WiFi.h>
#include <WebServer.h>
#include <Middlewares.h>

// Your AP WiFi Credentials
// ( This is the AP your ESP will broadcast )
const char *ap_ssid = "ESP32_Demo";
const char *ap_password = "";

WebServer server(80);

LoggingMiddleware logger;
CorsMiddleware cors;
AuthenticationMiddleware auth;

void setup(void) {
Serial.begin(115200);
WiFi.softAP(ap_ssid, ap_password);

Serial.print("IP address: ");
Serial.println(WiFi.AP.localIP());

// curl-like output example:
//
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
//
// Connection from 192.168.4.2:51683
// > OPTIONS / HTTP/1.1
// > Host: 192.168.4.1
// > User-Agent: curl/8.10.0
// > Accept: */*
// > origin: http://192.168.4.1
// >
// * Processed in 5 ms
// < HTTP/1.HTTP/1.1 200 OK
// < Content-Type: text/html
// < Access-Control-Allow-Origin: http://192.168.4.1
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
// < Access-Control-Allow-Headers: X-Custom-Header
// < Access-Control-Allow-Credentials: false
// < Access-Control-Max-Age: 600
// < Content-Length: 0
// < Connection: close
// <
logger.setOutput(Serial);

cors.setOrigin("http://192.168.4.1");
cors.setMethods("POST,GET,OPTIONS,DELETE");
cors.setHeaders("X-Custom-Header");
cors.setAllowCredentials(false);
cors.setMaxAge(600);

auth.setUsername("admin");
auth.setPassword("admin");
auth.setRealm("My Super App");
auth.setAuthMethod(DIGEST_AUTH);
auth.setAuthFailureMessage("Authentication Failed");

server.addMiddleware(&logger);
server.addMiddleware(&cors);

// Not authenticated
//
// Test CORS preflight request with:
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
//
// Test cross-domain request with:
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/
//
server.on("/", []() {
server.send(200, "text/plain", "Home");
});

// Authenticated
//
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/protected
//
// Outputs:
//
// * Connection from 192.168.4.2:51750
// > GET /protected HTTP/1.1
// > Host: 192.168.4.1
// > User-Agent: curl/8.10.0
// > Accept: */*
// > origin: http://192.168.4.1
// >
// * Processed in 7 ms
// < HTTP/1.HTTP/1.1 401 Unauthorized
// < Content-Type: text/html
// < Access-Control-Allow-Origin: http://192.168.4.1
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
// < Access-Control-Allow-Headers: X-Custom-Header
// < Access-Control-Allow-Credentials: false
// < Access-Control-Max-Age: 600
// < WWW-Authenticate: Digest realm="My Super App", qop="auth", nonce="ac388a64184e3e102aae6fff1c9e8d76", opaque="e7d158f2b54d25328142d118ff0f932d"
// < Content-Length: 21
// < Connection: close
// <
//
// > curl -v -X GET -H "origin: http://192.168.4.1" --digest -u admin:admin http://192.168.4.1/protected
//
// Outputs:
//
// * Connection from 192.168.4.2:53662
// > GET /protected HTTP/1.1
// > Authorization: Digest username="admin", realm="My Super App", nonce="db9e6824eb2a13bc7b2bf8f3c43db896", uri="/protected", cnonce="NTliZDZiNTcwODM2MzAyY2JjMDBmZGJmNzFiY2ZmNzk=", nc=00000001, qop=auth, response="6ebd145ba0d3496a4a73f5ae79ff5264", opaque="23d739c22810282ff820538cba98bda4"
// > Host: 192.168.4.1
// > User-Agent: curl/8.10.0
// > Accept: */*
// > origin: http://192.168.4.1
// >
// Request handling...
// * Processed in 7 ms
// < HTTP/1.HTTP/1.1 200 OK
// < Content-Type: text/plain
// < Access-Control-Allow-Origin: http://192.168.4.1
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
// < Access-Control-Allow-Headers: X-Custom-Header
// < Access-Control-Allow-Credentials: false
// < Access-Control-Max-Age: 600
// < Content-Length: 9
// < Connection: close
// <
server
.on(
"/protected",
[]() {
Serial.println("Request handling...");
server.send(200, "text/plain", "Protected");
}
)
.addMiddleware(&auth);

// Not found is also handled by global middleware
//
// curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/inexsting
//
// Outputs:
//
// * Connection from 192.168.4.2:53683
// > GET /inexsting HTTP/1.1
// > Host: 192.168.4.1
// > User-Agent: curl/8.10.0
// > Accept: */*
// > origin: http://192.168.4.1
// >
// * Processed in 16 ms
// < HTTP/1.HTTP/1.1 404 Not Found
// < Content-Type: text/plain
// < Access-Control-Allow-Origin: http://192.168.4.1
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
// < Access-Control-Allow-Headers: X-Custom-Header
// < Access-Control-Allow-Credentials: false
// < Access-Control-Max-Age: 600
// < Content-Length: 14
// < Connection: close
// <
server.onNotFound([]() {
server.send(404, "text/plain", "Page not found");
});

server.collectAllHeaders();
server.begin();
Serial.println("HTTP server started");
}

void loop(void) {
server.handleClient();
delay(2); //allow the cpu to switch to other tasks
}
5 changes: 5 additions & 0 deletions libraries/WebServer/examples/Middleware/ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"requires": [
"CONFIG_SOC_WIFI_SUPPORTED=y"
]
}
66 changes: 66 additions & 0 deletions libraries/WebServer/src/Middlewares.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef MIDDLEWARES_H
#define MIDDLEWARES_H

#include <WebServer.h>
#include <Stream.h>

#include <assert.h>

// curl-like logging middleware
class LoggingMiddleware : public Middleware {
public:
void setOutput(Print &output);

bool run(WebServer &server, Middleware::Callback next) override;

private:
Print *_out = nullptr;
};

class CorsMiddleware : public Middleware {
public:
CorsMiddleware &setOrigin(const char *origin);
CorsMiddleware &setMethods(const char *methods);
CorsMiddleware &setHeaders(const char *headers);
CorsMiddleware &setAllowCredentials(bool credentials);
CorsMiddleware &setMaxAge(uint32_t seconds);

void addCORSHeaders(WebServer &server);

bool run(WebServer &server, Middleware::Callback next) override;

private:
String _origin = F("*");
String _methods = F("*");
String _headers = F("*");
bool _credentials = true;
uint32_t _maxAge = 86400;
};

class AuthenticationMiddleware : public Middleware {
public:
AuthenticationMiddleware &setUsername(const char *username);
AuthenticationMiddleware &setPassword(const char *password);
AuthenticationMiddleware &setPasswordHash(const char *sha1AsBase64orHex);
AuthenticationMiddleware &setCallback(WebServer::THandlerFunctionAuthCheck fn);

AuthenticationMiddleware &setRealm(const char *realm);
AuthenticationMiddleware &setAuthMethod(HTTPAuthMethod method);
AuthenticationMiddleware &setAuthFailureMessage(const char *message);

bool isAllowed(WebServer &server) const;

bool run(WebServer &server, Middleware::Callback next) override;

private:
String _username;
String _password;
bool _hash = false;
WebServer::THandlerFunctionAuthCheck _callback;

const char *_realm = nullptr;
HTTPAuthMethod _method = BASIC_AUTH;
String _authFailMsg;
};

#endif
39 changes: 28 additions & 11 deletions libraries/WebServer/src/Parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ bool WebServer::_parseRequest(NetworkClient &client) {
String req = client.readStringUntil('\r');
client.readStringUntil('\n');
//reset header value
for (int i = 0; i < _headerKeysCount; ++i) {
_currentHeaders[i].value = String();
if (_collectAllHeaders) {
// clear previous headers
collectAllHeaders();
} else {
// clear previous headers
for (RequestArgument *header = _currentHeaders; header; header = header->next) {
header->value = String();
}
}

// First line of HTTP request looks like "GET /path HTTP/1.1"
Expand Down Expand Up @@ -154,9 +160,6 @@ bool WebServer::_parseRequest(NetworkClient &client) {
headerValue.trim();
_collectHeader(headerName.c_str(), headerValue.c_str());

log_v("headerName: %s", headerName.c_str());
log_v("headerValue: %s", headerValue.c_str());

if (headerName.equalsIgnoreCase(FPSTR(Content_Type))) {
using namespace mime;
if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))) {
Expand Down Expand Up @@ -254,9 +257,6 @@ bool WebServer::_parseRequest(NetworkClient &client) {
headerValue = req.substring(headerDiv + 2);
_collectHeader(headerName.c_str(), headerValue.c_str());

log_v("headerName: %s", headerName.c_str());
log_v("headerValue: %s", headerValue.c_str());

if (headerName.equalsIgnoreCase("Host")) {
_hostHeader = headerValue;
}
Expand All @@ -272,12 +272,29 @@ bool WebServer::_parseRequest(NetworkClient &client) {
}

bool WebServer::_collectHeader(const char *headerName, const char *headerValue) {
for (int i = 0; i < _headerKeysCount; i++) {
if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
_currentHeaders[i].value = headerValue;
RequestArgument *last = nullptr;
for (RequestArgument *header = _currentHeaders; header; header = header->next) {
if (header->next == nullptr) {
last = header;
}
if (header->key.equalsIgnoreCase(headerName)) {
header->value = headerValue;
log_v("header collected: %s: %s", headerName, headerValue);
return true;
}
}
assert(last);
if (_collectAllHeaders) {
last->next = new RequestArgument();
last->next->key = headerName;
last->next->value = headerValue;
_headerKeysCount++;
log_v("header collected: %s: %s", headerName, headerValue);
return true;
}

log_v("header skipped: %s: %s", headerName, headerValue);

return false;
}

Expand Down
Loading

0 comments on commit a3d10e9

Please sign in to comment.