Skip to content

Commit cf292b5

Browse files
committed
feat: Middleware support for WebServer
1 parent def319a commit cf292b5

File tree

9 files changed

+729
-85
lines changed

9 files changed

+729
-85
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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(Serial);
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+
cors.origin("http://192.168.4.1");
24+
cors.methods("POST, GET, OPTIONS, DELETE");
25+
cors.headers("X-Custom-Header");
26+
cors.allowCredentials(false);
27+
cors.maxAge(600);
28+
29+
auth.authenticate("admin", "admin");
30+
31+
server
32+
.on(
33+
"/",
34+
[]() {
35+
server.send(200, "text/plain", "Home");
36+
}
37+
)
38+
.addMiddleware(&logger)
39+
.addMiddleware(&cors)
40+
.addMiddleware(&auth);
41+
42+
server.onNotFound([]() {
43+
server.send(404, "text/plain", "Page not found");
44+
});
45+
46+
server.collectAllHeaders();
47+
server.begin();
48+
Serial.println("HTTP server started");
49+
}
50+
51+
void loop(void) {
52+
server.handleClient();
53+
delay(2); //allow the cpu to switch to other tasks
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
This example shows how to load all request headers and use middleware.
2+
3+
### CORS Middleware
4+
5+
```bash
6+
❯ curl -i -X OPTIONS http://192.168.4.1
7+
HTTP/1.1 200 OK
8+
Content-Type: text/html
9+
Access-Control-Allow-Origin: http://192.168.4.1
10+
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
11+
Access-Control-Allow-Headers: X-Custom-Header
12+
Access-Control-Allow-Credentials: false
13+
Access-Control-Max-Age: 600
14+
Content-Length: 0
15+
Connection: close
16+
```
17+
18+
Output of logger middleware:
19+
20+
```
21+
* Connection from 192.168.4.2:57597
22+
< OPTIONS / HTTP/1.1
23+
< Host: 192.168.4.1
24+
< User-Agent: curl/8.9.1
25+
< Accept: */*
26+
<
27+
* Processed!
28+
> HTTP/1.HTTP/1.1 200 OK
29+
> Content-Type: text/html
30+
> Access-Control-Allow-Origin: http://192.168.4.1
31+
> Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
32+
> Access-Control-Allow-Headers: X-Custom-Header
33+
> Access-Control-Allow-Credentials: false
34+
> Access-Control-Max-Age: 600
35+
> Content-Length: 0
36+
> Connection: close
37+
>
38+
```
39+
40+
### Authentication Middleware
41+
42+
```bash
43+
❯ curl -i -X GET http://192.168.4.1
44+
HTTP/1.1 401 Unauthorized
45+
Content-Type: text/html
46+
WWW-Authenticate: Basic realm=""
47+
Content-Length: 0
48+
Connection: close
49+
```
50+
51+
Output of logger middleware:
52+
53+
```
54+
* Connection from 192.168.4.2:57705
55+
< GET / HTTP/1.1
56+
< Host: 192.168.4.1
57+
< User-Agent: curl/8.9.1
58+
< Accept: */*
59+
<
60+
* Processed!
61+
> HTTP/1.HTTP/1.1 401 Unauthorized
62+
> Content-Type: text/html
63+
> WWW-Authenticate: Basic realm=""
64+
> Content-Length: 0
65+
> Connection: close
66+
>
67+
```
68+
69+
Sending auth...
70+
71+
```bash
72+
Note: Unnecessary use of -X or --request, GET is already inferred.
73+
* Trying 192.168.4.1:80...
74+
* Connected to 192.168.4.1 (192.168.4.1) port 80
75+
* Server auth using Basic with user 'admin'
76+
> GET / HTTP/1.1
77+
> Host: 192.168.4.1
78+
> Authorization: Basic YWRtaW46YWRtaW4=
79+
> User-Agent: curl/8.9.1
80+
> Accept: */*
81+
>
82+
* Request completely sent off
83+
< HTTP/1.1 200 OK
84+
< Content-Type: text/plain
85+
< Content-Length: 4
86+
< Connection: close
87+
<
88+
* shutting down connection #0
89+
Home
90+
91+
```
92+
93+
Output of logger middleware:
94+
95+
```
96+
* Connection from 192.168.4.2:62099
97+
< GET / HTTP/1.1
98+
< Authorization: Basic YWRtaW46YWRtaW4=
99+
< Host: 192.168.4.1
100+
< User-Agent: curl/8.9.1
101+
< Accept: */*
102+
<
103+
* Processed!
104+
> HTTP/1.HTTP/1.1 200 OK
105+
> Content-Type: text/plain
106+
> Content-Length: 4
107+
> Connection: close
108+
>
109+
```
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

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
private:
12+
Stream *_out;
13+
14+
public:
15+
explicit LoggingMiddleware(Stream &out) : _out(&out) {}
16+
17+
bool run(WebServer &server, Middleware::Callback next) override {
18+
_out->print(F("* Connection from "));
19+
_out->print(server.client().remoteIP().toString());
20+
_out->print(F(":"));
21+
_out->println(server.client().remotePort());
22+
23+
_out->print(F("< "));
24+
HTTPMethod method = server.method();
25+
if (method == HTTP_ANY) {
26+
_out->print(F("HTTP_ANY"));
27+
} else {
28+
_out->print(http_method_str((http_method)method));
29+
}
30+
_out->print(F(" "));
31+
_out->print(server.uri());
32+
_out->print(F(" "));
33+
_out->println(server.version());
34+
35+
int n = server.headers();
36+
for (int i = 0; i < n; i++) {
37+
String v = server.header(i);
38+
if (!v.isEmpty()) {
39+
// because these 2 are always there, eventually empty: "Authorization", "If-None-Match"
40+
_out->print(F("< "));
41+
_out->print(server.headerName(i));
42+
_out->print(F(": "));
43+
_out->println(server.header(i));
44+
}
45+
}
46+
47+
_out->println(F("<"));
48+
49+
bool ret = next();
50+
51+
if (ret) {
52+
_out->println(F("* Processed!"));
53+
54+
_out->print(F("> "));
55+
_out->print(F("HTTP/1."));
56+
_out->print(server.version());
57+
_out->print(F(" "));
58+
_out->print(server.responseCode());
59+
_out->print(F(" "));
60+
_out->println(WebServer::responseCodeToString(server.responseCode()));
61+
62+
n = server.responseHeaders();
63+
for (int i = 0; i < n; i++) {
64+
_out->print(F("> "));
65+
_out->print(server.responseHeaderName(i));
66+
_out->print(F(": "));
67+
_out->println(server.responseHeader(i));
68+
}
69+
70+
_out->println(F(">"));
71+
72+
} else {
73+
_out->println(F("* Not processed!"));
74+
}
75+
76+
return ret;
77+
}
78+
};
79+
80+
class AuthenticationMiddleware : public Middleware {
81+
private:
82+
// authenticate state
83+
// 0: not authenticated
84+
// 1: callback
85+
// 2: username/password
86+
// 3: sha1
87+
int _auth = 0;
88+
WebServer::THandlerFunctionAuthCheck _fn;
89+
String _username;
90+
String _password;
91+
String _sha1;
92+
93+
// authenticate request
94+
HTTPAuthMethod _mode = BASIC_AUTH;
95+
String _realm;
96+
String _authFailMsg;
97+
98+
public:
99+
AuthenticationMiddleware &authenticate(WebServer::THandlerFunctionAuthCheck fn) {
100+
assert(fn);
101+
_fn = fn;
102+
_auth = 1;
103+
return *this;
104+
}
105+
106+
AuthenticationMiddleware &authenticate(const char *username, const char *password) {
107+
if (strlen(username) == 0 || strlen(password) == 0) {
108+
_auth = 0;
109+
return *this;
110+
} else {
111+
_username = username;
112+
_password = password;
113+
_auth = 2;
114+
return *this;
115+
}
116+
}
117+
118+
AuthenticationMiddleware &authenticateBasicSHA1(const char *username, const char *sha1AsBase64orHex) {
119+
if (strlen(username) == 0 || strlen(sha1AsBase64orHex) == 0) {
120+
_auth = 0;
121+
return *this;
122+
}
123+
_username = username;
124+
_sha1 = sha1AsBase64orHex;
125+
_auth = 3;
126+
return *this;
127+
}
128+
129+
bool run(WebServer &server, Middleware::Callback next) override {
130+
switch (_auth) {
131+
case 1:
132+
if (server.authenticate(_fn)) {
133+
return next();
134+
} else {
135+
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
136+
return true;
137+
}
138+
139+
case 2:
140+
if (server.authenticate(_username.c_str(), _password.c_str())) {
141+
return next();
142+
} else {
143+
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
144+
return true;
145+
}
146+
147+
case 3:
148+
if (server.authenticate(_username.c_str(), _sha1.c_str())) {
149+
return next();
150+
} else {
151+
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
152+
return true;
153+
}
154+
155+
default: return next();
156+
}
157+
}
158+
};
159+
160+
class CorsMiddleware : public Middleware {
161+
private:
162+
String _origin = F("*");
163+
String _methods = F("*");
164+
String _headers = F("*");
165+
bool _credentials = true;
166+
uint32_t _maxAge = 86400;
167+
168+
public:
169+
CorsMiddleware &origin(const char *origin) {
170+
_origin = origin;
171+
return *this;
172+
}
173+
174+
CorsMiddleware &methods(const char *methods) {
175+
_methods = methods;
176+
return *this;
177+
}
178+
179+
CorsMiddleware &headers(const char *headers) {
180+
_headers = headers;
181+
return *this;
182+
}
183+
184+
CorsMiddleware &allowCredentials(bool credentials) {
185+
_credentials = credentials;
186+
return *this;
187+
}
188+
189+
CorsMiddleware &maxAge(uint32_t seconds) {
190+
_maxAge = seconds;
191+
return *this;
192+
}
193+
194+
bool run(WebServer &server, Middleware::Callback next) override {
195+
if (server.method() == HTTP_OPTIONS) {
196+
server.sendHeader(F("Access-Control-Allow-Origin"), _origin.c_str());
197+
server.sendHeader(F("Access-Control-Allow-Methods"), _methods.c_str());
198+
server.sendHeader(F("Access-Control-Allow-Headers"), _headers.c_str());
199+
server.sendHeader(F("Access-Control-Allow-Credentials"), _credentials ? F("true") : F("false"));
200+
server.sendHeader(F("Access-Control-Max-Age"), String(_maxAge).c_str());
201+
server.send(200);
202+
return true;
203+
}
204+
return next();
205+
}
206+
};
207+
208+
#endif

0 commit comments

Comments
 (0)