Skip to content

Commit cf0f79a

Browse files
committed
feat(webserver): Middleware with default middleware for cors, authc, curl-like logging
1 parent ae052f4 commit cf0f79a

14 files changed

+881
-101
lines changed

CMakeLists.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,11 @@ set(ARDUINO_LIBRARY_USB_SRCS
212212
set(ARDUINO_LIBRARY_WebServer_SRCS
213213
libraries/WebServer/src/WebServer.cpp
214214
libraries/WebServer/src/Parsing.cpp
215-
libraries/WebServer/src/detail/mimetable.cpp)
215+
libraries/WebServer/src/detail/mimetable.cpp
216+
libraries/WebServer/src/middleware/MiddlewareChain.cpp
217+
libraries/WebServer/src/middleware/AuthenticationMiddleware.cpp
218+
libraries/WebServer/src/middleware/CorsMiddleware.cpp
219+
libraries/WebServer/src/middleware/LoggingMiddleware.cpp)
216220

217221
set(ARDUINO_LIBRARY_NetworkClientSecure_SRCS
218222
libraries/NetworkClientSecure/src/ssl_client.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#include <WiFi.h>
2+
#include <WebServer.h>
3+
#include <Middlewares.h>
4+
5+
// Your AP WiFi Credentials
6+
// ( This is the AP your ESP will broadcast )
7+
const char *ap_ssid = "ESP32_Demo";
8+
const char *ap_password = "";
9+
10+
WebServer server(80);
11+
12+
LoggingMiddleware logger;
13+
CorsMiddleware cors;
14+
AuthenticationMiddleware auth;
15+
16+
void setup(void) {
17+
Serial.begin(115200);
18+
WiFi.softAP(ap_ssid, ap_password);
19+
20+
Serial.print("IP address: ");
21+
Serial.println(WiFi.AP.localIP());
22+
23+
// curl-like output example:
24+
//
25+
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
26+
//
27+
// Connection from 192.168.4.2:51683
28+
// > OPTIONS / HTTP/1.1
29+
// > Host: 192.168.4.1
30+
// > User-Agent: curl/8.10.0
31+
// > Accept: */*
32+
// > origin: http://192.168.4.1
33+
// >
34+
// * Processed in 5 ms
35+
// < HTTP/1.HTTP/1.1 200 OK
36+
// < Content-Type: text/html
37+
// < Access-Control-Allow-Origin: http://192.168.4.1
38+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
39+
// < Access-Control-Allow-Headers: X-Custom-Header
40+
// < Access-Control-Allow-Credentials: false
41+
// < Access-Control-Max-Age: 600
42+
// < Content-Length: 0
43+
// < Connection: close
44+
// <
45+
logger.setOutput(Serial);
46+
47+
cors.setOrigin("http://192.168.4.1");
48+
cors.setMethods("POST,GET,OPTIONS,DELETE");
49+
cors.setHeaders("X-Custom-Header");
50+
cors.setAllowCredentials(false);
51+
cors.setMaxAge(600);
52+
53+
auth.setUsername("admin");
54+
auth.setPassword("admin");
55+
auth.setRealm("My Super App");
56+
auth.setAuthMethod(DIGEST_AUTH);
57+
auth.setAuthFailureMessage("Authentication Failed");
58+
59+
server.addMiddleware(&logger);
60+
server.addMiddleware(&cors);
61+
62+
// Not authenticated
63+
//
64+
// Test CORS preflight request with:
65+
// > curl -v -X OPTIONS -H "origin: http://192.168.4.1" http://192.168.4.1/
66+
//
67+
// Test cross-domain request with:
68+
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/
69+
//
70+
server.on("/", []() {
71+
server.send(200, "text/plain", "Home");
72+
});
73+
74+
// Authenticated
75+
//
76+
// > curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/protected
77+
//
78+
// Outputs:
79+
//
80+
// * Connection from 192.168.4.2:51750
81+
// > GET /protected HTTP/1.1
82+
// > Host: 192.168.4.1
83+
// > User-Agent: curl/8.10.0
84+
// > Accept: */*
85+
// > origin: http://192.168.4.1
86+
// >
87+
// * Processed in 7 ms
88+
// < HTTP/1.HTTP/1.1 401 Unauthorized
89+
// < Content-Type: text/html
90+
// < Access-Control-Allow-Origin: http://192.168.4.1
91+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
92+
// < Access-Control-Allow-Headers: X-Custom-Header
93+
// < Access-Control-Allow-Credentials: false
94+
// < Access-Control-Max-Age: 600
95+
// < WWW-Authenticate: Digest realm="My Super App", qop="auth", nonce="ac388a64184e3e102aae6fff1c9e8d76", opaque="e7d158f2b54d25328142d118ff0f932d"
96+
// < Content-Length: 21
97+
// < Connection: close
98+
// <
99+
//
100+
// > curl -v -X GET -H "origin: http://192.168.4.1" --digest -u admin:admin http://192.168.4.1/protected
101+
//
102+
// Outputs:
103+
//
104+
// * Connection from 192.168.4.2:53662
105+
// > GET /protected HTTP/1.1
106+
// > Authorization: Digest username="admin", realm="My Super App", nonce="db9e6824eb2a13bc7b2bf8f3c43db896", uri="/protected", cnonce="NTliZDZiNTcwODM2MzAyY2JjMDBmZGJmNzFiY2ZmNzk=", nc=00000001, qop=auth, response="6ebd145ba0d3496a4a73f5ae79ff5264", opaque="23d739c22810282ff820538cba98bda4"
107+
// > Host: 192.168.4.1
108+
// > User-Agent: curl/8.10.0
109+
// > Accept: */*
110+
// > origin: http://192.168.4.1
111+
// >
112+
// Request handling...
113+
// * Processed in 7 ms
114+
// < HTTP/1.HTTP/1.1 200 OK
115+
// < Content-Type: text/plain
116+
// < Access-Control-Allow-Origin: http://192.168.4.1
117+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
118+
// < Access-Control-Allow-Headers: X-Custom-Header
119+
// < Access-Control-Allow-Credentials: false
120+
// < Access-Control-Max-Age: 600
121+
// < Content-Length: 9
122+
// < Connection: close
123+
// <
124+
server
125+
.on(
126+
"/protected",
127+
[]() {
128+
Serial.println("Request handling...");
129+
server.send(200, "text/plain", "Protected");
130+
}
131+
)
132+
.addMiddleware(&auth);
133+
134+
// Not found is also handled by global middleware
135+
//
136+
// curl -v -X GET -H "origin: http://192.168.4.1" http://192.168.4.1/inexsting
137+
//
138+
// Outputs:
139+
//
140+
// * Connection from 192.168.4.2:53683
141+
// > GET /inexsting HTTP/1.1
142+
// > Host: 192.168.4.1
143+
// > User-Agent: curl/8.10.0
144+
// > Accept: */*
145+
// > origin: http://192.168.4.1
146+
// >
147+
// * Processed in 16 ms
148+
// < HTTP/1.HTTP/1.1 404 Not Found
149+
// < Content-Type: text/plain
150+
// < Access-Control-Allow-Origin: http://192.168.4.1
151+
// < Access-Control-Allow-Methods: POST,GET,OPTIONS,DELETE
152+
// < Access-Control-Allow-Headers: X-Custom-Header
153+
// < Access-Control-Allow-Credentials: false
154+
// < Access-Control-Max-Age: 600
155+
// < Content-Length: 14
156+
// < Connection: close
157+
// <
158+
server.onNotFound([]() {
159+
server.send(404, "text/plain", "Page not found");
160+
});
161+
162+
server.collectAllHeaders();
163+
server.begin();
164+
Serial.println("HTTP server started");
165+
}
166+
167+
void loop(void) {
168+
server.handleClient();
169+
delay(2); //allow the cpu to switch to other tasks
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"targets": {
3+
"esp32h2": false
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))) {
@@ -253,9 +256,6 @@ bool WebServer::_parseRequest(NetworkClient &client) {
253256
headerValue = req.substring(headerDiv + 2);
254257
_collectHeader(headerName.c_str(), headerValue.c_str());
255258

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

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

0 commit comments

Comments
 (0)