Skip to content

Commit b07eb17

Browse files
mathieucarbouSuGliderpre-commit-ci-lite[bot]
authored
feat(webserver): Middleware with default middleware for cors, authc, curl-like logging (#10750)
* feat(webserver): Middleware with default middleware for cors, authc, curl-like logging * ci(pre-commit): Apply automatic fixes --------- Co-authored-by: Rodrigo Garcia <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 089cbab commit b07eb17

14 files changed

+895
-101
lines changed

CMakeLists.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,11 @@ set(ARDUINO_LIBRARY_USB_SRCS
242242
set(ARDUINO_LIBRARY_WebServer_SRCS
243243
libraries/WebServer/src/WebServer.cpp
244244
libraries/WebServer/src/Parsing.cpp
245-
libraries/WebServer/src/detail/mimetable.cpp)
245+
libraries/WebServer/src/detail/mimetable.cpp
246+
libraries/WebServer/src/middleware/MiddlewareChain.cpp
247+
libraries/WebServer/src/middleware/AuthenticationMiddleware.cpp
248+
libraries/WebServer/src/middleware/CorsMiddleware.cpp
249+
libraries/WebServer/src/middleware/LoggingMiddleware.cpp)
246250

247251
set(ARDUINO_LIBRARY_NetworkClientSecure_SRCS
248252
libraries/NetworkClientSecure/src/ssl_client.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Basic example of using Middlewares with WebServer
3+
*
4+
* Middleware are common request/response processing functions that can be applied globally to all incoming requests or to specific handlers.
5+
* They allow for a common processing thus saving memory and space to avoid duplicating code or states on multiple handlers.
6+
*
7+
* Once the example is flashed (with the correct WiFi credentials), you can test the following scenarios with the listed curl commands:
8+
* - 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
9+
* - Logging Middleware: logs the request and response to an output in a curl-like format
10+
* - Authentication Middleware: test the authentication with Digest Auth
11+
*
12+
* You can also add your own Middleware by extending the Middleware class and implementing the run method.
13+
* When implementing a Middleware, you can decide when to call the next Middleware in the chain by calling next().
14+
*
15+
* Middleware are execute in order of addition, the ones attached to the server will be executed first.
16+
*/
17+
#include <WiFi.h>
18+
#include <WebServer.h>
19+
#include <Middlewares.h>
20+
21+
// Your AP WiFi Credentials
22+
// ( This is the AP your ESP will broadcast )
23+
const char *ap_ssid = "ESP32_Demo";
24+
const char *ap_password = "";
25+
26+
WebServer server(80);
27+
28+
LoggingMiddleware logger;
29+
CorsMiddleware cors;
30+
AuthenticationMiddleware auth;
31+
32+
void setup(void) {
33+
Serial.begin(115200);
34+
WiFi.softAP(ap_ssid, ap_password);
35+
36+
Serial.print("IP address: ");
37+
Serial.println(WiFi.AP.localIP());
38+
39+
// curl-like output example:
40+
//
41+
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
42+
//
43+
// Connection from 192.168.4.2:51683
44+
// > OPTIONS / HTTP/1.1
45+
// > Host: 192.168.4.1
46+
// > User-Agent: curl/8.10.0
47+
// > Accept: */*
48+
// > origin: http://192.168.4.1
49+
// >
50+
// * Processed in 5 ms
51+
// < HTTP/1.HTTP/1.1 200 OK
52+
// < Content-Type: text/html
53+
// < Access-Control-Allow-Origin: http://192.168.4.1
54+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
55+
// < Access-Control-Allow-Headers: X-Custom-Header
56+
// < Access-Control-Allow-Credentials: false
57+
// < Access-Control-Max-Age: 600
58+
// < Content-Length: 0
59+
// < Connection: close
60+
// <
61+
logger.setOutput(Serial);
62+
63+
cors.setOrigin("http://192.168.4.1");
64+
cors.setMethods("POST,GET,OPTIONS,DELETE");
65+
cors.setHeaders("X-Custom-Header");
66+
cors.setAllowCredentials(false);
67+
cors.setMaxAge(600);
68+
69+
auth.setUsername("admin");
70+
auth.setPassword("admin");
71+
auth.setRealm("My Super App");
72+
auth.setAuthMethod(DIGEST_AUTH);
73+
auth.setAuthFailureMessage("Authentication Failed");
74+
75+
server.addMiddleware(&logger);
76+
server.addMiddleware(&cors);
77+
78+
// Not authenticated
79+
//
80+
// Test CORS preflight request with:
81+
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
82+
//
83+
// Test cross-domain request with:
84+
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/
85+
//
86+
server.on("/", []() {
87+
server.send(200, "text/plain", "Home");
88+
});
89+
90+
// Authenticated
91+
//
92+
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/protected
93+
//
94+
// Outputs:
95+
//
96+
// * Connection from 192.168.4.2:51750
97+
// > GET /protected HTTP/1.1
98+
// > Host: 192.168.4.1
99+
// > User-Agent: curl/8.10.0
100+
// > Accept: */*
101+
// > origin: http://192.168.4.1
102+
// >
103+
// * Processed in 7 ms
104+
// < HTTP/1.HTTP/1.1 401 Unauthorized
105+
// < Content-Type: text/html
106+
// < Access-Control-Allow-Origin: http://192.168.4.1
107+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
108+
// < Access-Control-Allow-Headers: X-Custom-Header
109+
// < Access-Control-Allow-Credentials: false
110+
// < Access-Control-Max-Age: 600
111+
// < WWW-Authenticate: Digest realm="My Super App", qop="auth", nonce="ac388a64184e3e102aae6fff1c9e8d76", opaque="e7d158f2b54d25328142d118ff0f932d"
112+
// < Content-Length: 21
113+
// < Connection: close
114+
// <
115+
//
116+
// > curl -v -X GET -H "origin: http://192.168.4.1" --digest -u admin:admin http://192.168.4.1/protected
117+
//
118+
// Outputs:
119+
//
120+
// * Connection from 192.168.4.2:53662
121+
// > GET /protected HTTP/1.1
122+
// > Authorization: Digest username="admin", realm="My Super App", nonce="db9e6824eb2a13bc7b2bf8f3c43db896", uri="/protected", cnonce="NTliZDZiNTcwODM2MzAyY2JjMDBmZGJmNzFiY2ZmNzk=", nc=00000001, qop=auth, response="6ebd145ba0d3496a4a73f5ae79ff5264", opaque="23d739c22810282ff820538cba98bda4"
123+
// > Host: 192.168.4.1
124+
// > User-Agent: curl/8.10.0
125+
// > Accept: */*
126+
// > origin: http://192.168.4.1
127+
// >
128+
// Request handling...
129+
// * Processed in 7 ms
130+
// < HTTP/1.HTTP/1.1 200 OK
131+
// < Content-Type: text/plain
132+
// < Access-Control-Allow-Origin: http://192.168.4.1
133+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
134+
// < Access-Control-Allow-Headers: X-Custom-Header
135+
// < Access-Control-Allow-Credentials: false
136+
// < Access-Control-Max-Age: 600
137+
// < Content-Length: 9
138+
// < Connection: close
139+
// <
140+
server
141+
.on(
142+
"/protected",
143+
[]() {
144+
Serial.println("Request handling...");
145+
server.send(200, "text/plain", "Protected");
146+
}
147+
)
148+
.addMiddleware(&auth);
149+
150+
// Not found is also handled by global middleware
151+
//
152+
// curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/inexsting
153+
//
154+
// Outputs:
155+
//
156+
// * Connection from 192.168.4.2:53683
157+
// > GET /inexsting HTTP/1.1
158+
// > Host: 192.168.4.1
159+
// > User-Agent: curl/8.10.0
160+
// > Accept: */*
161+
// > origin: http://192.168.4.1
162+
// >
163+
// * Processed in 16 ms
164+
// < HTTP/1.HTTP/1.1 404 Not Found
165+
// < Content-Type: text/plain
166+
// < Access-Control-Allow-Origin: http://192.168.4.1
167+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
168+
// < Access-Control-Allow-Headers: X-Custom-Header
169+
// < Access-Control-Allow-Credentials: false
170+
// < Access-Control-Max-Age: 600
171+
// < Content-Length: 14
172+
// < Connection: close
173+
// <
174+
server.onNotFound([]() {
175+
server.send(404, "text/plain", "Page not found");
176+
});
177+
178+
server.collectAllHeaders();
179+
server.begin();
180+
Serial.println("HTTP server started");
181+
}
182+
183+
void loop(void) {
184+
server.handleClient();
185+
delay(2); //allow the cpu to switch to other tasks
186+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"requires": [
3+
"CONFIG_SOC_WIFI_SUPPORTED=y"
4+
]
5+
}

libraries/WebServer/src/Middlewares.h

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#ifndef MIDDLEWARES_H
2+
#define MIDDLEWARES_H
3+
4+
#include <WebServer.h>
5+
#include <Stream.h>
6+
7+
#include <assert.h>
8+
9+
// curl-like logging middleware
10+
class LoggingMiddleware : public Middleware {
11+
public:
12+
void setOutput(Print &output);
13+
14+
bool run(WebServer &server, Middleware::Callback next) override;
15+
16+
private:
17+
Print *_out = nullptr;
18+
};
19+
20+
class CorsMiddleware : public Middleware {
21+
public:
22+
CorsMiddleware &setOrigin(const char *origin);
23+
CorsMiddleware &setMethods(const char *methods);
24+
CorsMiddleware &setHeaders(const char *headers);
25+
CorsMiddleware &setAllowCredentials(bool credentials);
26+
CorsMiddleware &setMaxAge(uint32_t seconds);
27+
28+
void addCORSHeaders(WebServer &server);
29+
30+
bool run(WebServer &server, Middleware::Callback next) override;
31+
32+
private:
33+
String _origin = F("*");
34+
String _methods = F("*");
35+
String _headers = F("*");
36+
bool _credentials = true;
37+
uint32_t _maxAge = 86400;
38+
};
39+
40+
class AuthenticationMiddleware : public Middleware {
41+
public:
42+
AuthenticationMiddleware &setUsername(const char *username);
43+
AuthenticationMiddleware &setPassword(const char *password);
44+
AuthenticationMiddleware &setPasswordHash(const char *sha1AsBase64orHex);
45+
AuthenticationMiddleware &setCallback(WebServer::THandlerFunctionAuthCheck fn);
46+
47+
AuthenticationMiddleware &setRealm(const char *realm);
48+
AuthenticationMiddleware &setAuthMethod(HTTPAuthMethod method);
49+
AuthenticationMiddleware &setAuthFailureMessage(const char *message);
50+
51+
bool isAllowed(WebServer &server) const;
52+
53+
bool run(WebServer &server, Middleware::Callback next) override;
54+
55+
private:
56+
String _username;
57+
String _password;
58+
bool _hash = false;
59+
WebServer::THandlerFunctionAuthCheck _callback;
60+
61+
const char *_realm = nullptr;
62+
HTTPAuthMethod _method = BASIC_AUTH;
63+
String _authFailMsg;
64+
};
65+
66+
#endif

libraries/WebServer/src/Parsing.cpp

+28-11
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,14 @@ bool WebServer::_parseRequest(NetworkClient &client) {
7878
String req = client.readStringUntil('\r');
7979
client.readStringUntil('\n');
8080
//reset header value
81-
for (int i = 0; i < _headerKeysCount; ++i) {
82-
_currentHeaders[i].value = String();
81+
if (_collectAllHeaders) {
82+
// clear previous headers
83+
collectAllHeaders();
84+
} else {
85+
// clear previous headers
86+
for (RequestArgument *header = _currentHeaders; header; header = header->next) {
87+
header->value = String();
88+
}
8389
}
8490

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

157-
log_v("headerName: %s", headerName.c_str());
158-
log_v("headerValue: %s", headerValue.c_str());
159-
160163
if (headerName.equalsIgnoreCase(FPSTR(Content_Type))) {
161164
using namespace mime;
162165
if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))) {
@@ -254,9 +257,6 @@ bool WebServer::_parseRequest(NetworkClient &client) {
254257
headerValue = req.substring(headerDiv + 2);
255258
_collectHeader(headerName.c_str(), headerValue.c_str());
256259

257-
log_v("headerName: %s", headerName.c_str());
258-
log_v("headerValue: %s", headerValue.c_str());
259-
260260
if (headerName.equalsIgnoreCase("Host")) {
261261
_hostHeader = headerValue;
262262
}
@@ -272,12 +272,29 @@ bool WebServer::_parseRequest(NetworkClient &client) {
272272
}
273273

274274
bool WebServer::_collectHeader(const char *headerName, const char *headerValue) {
275-
for (int i = 0; i < _headerKeysCount; i++) {
276-
if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
277-
_currentHeaders[i].value = headerValue;
275+
RequestArgument *last = nullptr;
276+
for (RequestArgument *header = _currentHeaders; header; header = header->next) {
277+
if (header->next == nullptr) {
278+
last = header;
279+
}
280+
if (header->key.equalsIgnoreCase(headerName)) {
281+
header->value = headerValue;
282+
log_v("header collected: %s: %s", headerName, headerValue);
278283
return true;
279284
}
280285
}
286+
assert(last);
287+
if (_collectAllHeaders) {
288+
last->next = new RequestArgument();
289+
last->next->key = headerName;
290+
last->next->value = headerValue;
291+
_headerKeysCount++;
292+
log_v("header collected: %s: %s", headerName, headerValue);
293+
return true;
294+
}
295+
296+
log_v("header skipped: %s: %s", headerName, headerValue);
297+
281298
return false;
282299
}
283300

0 commit comments

Comments
 (0)