Skip to content

Commit 4c23431

Browse files
Add hook support to WebServer (#1119)
Implement the method used in the ESP8266 Web Server to allow user apps to hook into the HTTP server (to support hooked WebSockets, etc._) Add example of hook usage
1 parent f7ee4a8 commit 4c23431

File tree

4 files changed

+209
-18
lines changed

4 files changed

+209
-18
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#include <WiFi.h>
2+
#include <WiFiClient.h>
3+
#include <WebServer.h>
4+
#include <LEAmDNS.h>
5+
6+
#ifndef STASSID
7+
#define STASSID "your-ssid"
8+
#define STAPSK "your-password"
9+
#endif
10+
11+
const char* ssid = STASSID;
12+
const char* password = STAPSK;
13+
14+
WebServer server(80);
15+
16+
const int led = LED_BUILTIN;
17+
18+
void handleRoot() {
19+
digitalWrite(led, 1);
20+
server.send(200, "text/plain", "hello from pico w!\r\n");
21+
digitalWrite(led, 0);
22+
}
23+
24+
void handleNotFound() {
25+
digitalWrite(led, 1);
26+
String message = "File Not Found\n\n";
27+
message += "URI: ";
28+
message += server.uri();
29+
message += "\nMethod: ";
30+
message += (server.method() == HTTP_GET) ? "GET" : "POST";
31+
message += "\nArguments: ";
32+
message += server.args();
33+
message += "\n";
34+
for (uint8_t i = 0; i < server.args(); i++) {
35+
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
36+
}
37+
server.send(404, "text/plain", message);
38+
digitalWrite(led, 0);
39+
}
40+
41+
void setup(void) {
42+
pinMode(led, OUTPUT);
43+
digitalWrite(led, 0);
44+
Serial.begin(115200);
45+
WiFi.mode(WIFI_STA);
46+
WiFi.begin(ssid, password);
47+
Serial.println("");
48+
49+
// Wait for connection
50+
while (WiFi.status() != WL_CONNECTED) {
51+
delay(500);
52+
Serial.print(".");
53+
}
54+
Serial.println("");
55+
Serial.print("Connected to ");
56+
Serial.println(ssid);
57+
Serial.print("IP address: ");
58+
Serial.println(WiFi.localIP());
59+
60+
if (MDNS.begin("picow")) {
61+
Serial.println("MDNS responder started");
62+
}
63+
64+
server.on("/", handleRoot);
65+
66+
server.on("/inline", []() {
67+
server.send(200, "text/plain", "this works as well");
68+
});
69+
70+
server.on("/gif", []() {
71+
static const uint8_t gif[] = {
72+
0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80, 0x01,
73+
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x00,
74+
0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x19, 0x8c, 0x8f, 0xa9, 0xcb, 0x9d,
75+
0x00, 0x5f, 0x74, 0xb4, 0x56, 0xb0, 0xb0, 0xd2, 0xf2, 0x35, 0x1e, 0x4c,
76+
0x0c, 0x24, 0x5a, 0xe6, 0x89, 0xa6, 0x4d, 0x01, 0x00, 0x3b
77+
};
78+
char gif_colored[sizeof(gif)];
79+
memcpy_P(gif_colored, gif, sizeof(gif));
80+
// Set the background to a random set of colors
81+
gif_colored[16] = millis() % 256;
82+
gif_colored[17] = millis() % 256;
83+
gif_colored[18] = millis() % 256;
84+
server.send(200, "image/gif", gif_colored, sizeof(gif_colored));
85+
});
86+
87+
server.onNotFound(handleNotFound);
88+
89+
/////////////////////////////////////////////////////////
90+
// Hook examples
91+
92+
server.addHook([](const String & method, const String & url, WiFiClient * client, WebServer::ContentTypeFunction contentType) {
93+
(void)method; // GET, PUT, ...
94+
(void)url; // example: /root/myfile.html
95+
(void)client; // the webserver tcp client connection
96+
(void)contentType; // contentType(".html") => "text/html"
97+
Serial.printf("A useless web hook has passed\n");
98+
return WebServer::CLIENT_REQUEST_CAN_CONTINUE;
99+
});
100+
101+
server.addHook([](const String&, const String & url, WiFiClient*, WebServer::ContentTypeFunction) {
102+
if (url.startsWith("/fail")) {
103+
Serial.printf("An always failing web hook has been triggered\n");
104+
return WebServer::CLIENT_MUST_STOP;
105+
}
106+
return WebServer::CLIENT_REQUEST_CAN_CONTINUE;
107+
});
108+
109+
server.addHook([](const String&, const String & url, WiFiClient * client, WebServer::ContentTypeFunction) {
110+
if (url.startsWith("/dump")) {
111+
Serial.printf("The dumper web hook is on the run\n");
112+
113+
// Here the request is not interpreted, so we cannot for sure
114+
// swallow the exact amount matching the full request+content,
115+
// hence the tcp connection cannot be handled anymore by the
116+
auto last = millis();
117+
while ((millis() - last) < 500) {
118+
char buf[32];
119+
size_t len = client->read((uint8_t*)buf, sizeof(buf));
120+
if (len > 0) {
121+
Serial.printf("(<%d> chars)", (int)len);
122+
Serial.write(buf, len);
123+
last = millis();
124+
}
125+
}
126+
// Two choices: return MUST STOP and webserver will close it
127+
// (we already have the example with '/fail' hook)
128+
// or IS GIVEN and webserver will forget it
129+
// trying with IS GIVEN and storing it on a dumb WiFiClient.
130+
// check the client connection: it should not immediately be closed
131+
// (make another '/dump' one to close the first)
132+
Serial.printf("\nTelling server to forget this connection\n");
133+
static WiFiClient forgetme = *client; // stop previous one if present and transfer client refcounter
134+
return WebServer::CLIENT_IS_GIVEN;
135+
}
136+
return WebServer::CLIENT_REQUEST_CAN_CONTINUE;
137+
});
138+
139+
// Hook examples
140+
/////////////////////////////////////////////////////////
141+
142+
server.begin();
143+
Serial.println("HTTP server started");
144+
}
145+
146+
void loop(void) {
147+
server.handleClient();
148+
MDNS.update();
149+
}

libraries/WebServer/src/HTTPServer.cpp

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -249,20 +249,32 @@ void HTTPServer::httpHandleClient() {
249249
case HC_WAIT_READ:
250250
// Wait for data from client to become available
251251
if (_currentClient->available()) {
252-
if (_parseRequest(_currentClient)) {
253-
// because HTTP_MAX_SEND_WAIT is expressed in milliseconds,
254-
// it must be divided by 1000
252+
switch (_parseRequest(_currentClient)) {
253+
case CLIENT_REQUEST_CAN_CONTINUE:
254+
// Because HTTP_MAX_SEND_WAIT is expressed in milliseconds, it must be divided by 1000
255255
_currentClient->setTimeout(HTTP_MAX_SEND_WAIT / 1000);
256256
_contentLength = CONTENT_LENGTH_NOT_SET;
257257
_handleRequest();
258-
259-
// Fix for issue with Chrome based browsers: https://github.com/espressif/arduino-esp32/issues/3652
260-
// if (_currentClient->connected()) {
261-
// _currentStatus = HC_WAIT_CLOSE;
262-
// _statusChange = millis();
263-
// keepCurrentClient = true;
264-
// }
265-
}
258+
/* fallthrough */
259+
case CLIENT_REQUEST_IS_HANDLED:
260+
if (_currentClient->connected() || _currentClient->available()) {
261+
_currentStatus = HC_WAIT_CLOSE;
262+
_statusChange = millis();
263+
keepCurrentClient = true;
264+
} else {
265+
log_v("webserver: peer has closed after served\n");
266+
}
267+
break;
268+
case CLIENT_MUST_STOP:
269+
log_v("Close client\n");
270+
_currentClient->stop();
271+
break;
272+
case CLIENT_IS_GIVEN:
273+
// client must not be stopped but must not be handled here anymore
274+
// (example: tcp connection given to websocket)
275+
log_v("Give client\n");
276+
break;
277+
} // switch _parseRequest()
266278
} else { // !_currentClient->available()
267279
if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) {
268280
keepCurrentClient = true;

libraries/WebServer/src/HTTPServer.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH };
4848
#define CONTENT_LENGTH_UNKNOWN ((size_t) -1)
4949
#define CONTENT_LENGTH_NOT_SET ((size_t) -2)
5050

51+
#define WEBSERVER_HAS_HOOK 1
52+
5153
class HTTPServer;
5254

5355
typedef struct {
@@ -183,6 +185,25 @@ class HTTPServer {
183185
return _currentClient->write(file);
184186
}
185187

188+
// Hook
189+
enum ClientFuture { CLIENT_REQUEST_CAN_CONTINUE, CLIENT_REQUEST_IS_HANDLED, CLIENT_MUST_STOP, CLIENT_IS_GIVEN };
190+
typedef String(*ContentTypeFunction)(const String&);
191+
using HookFunction = std::function<ClientFuture(const String& method, const String& url, WiFiClient* client, ContentTypeFunction contentType)>;
192+
void addHook(HookFunction hook) {
193+
if (_hook) {
194+
auto previousHook = _hook;
195+
_hook = [previousHook, hook](const String & method, const String & url, WiFiClient * client, ContentTypeFunction contentType) {
196+
auto whatNow = previousHook(method, url, client, contentType);
197+
if (whatNow == CLIENT_REQUEST_CAN_CONTINUE) {
198+
return hook(method, url, client, contentType);
199+
}
200+
return whatNow;
201+
};
202+
} else {
203+
_hook = hook;
204+
}
205+
}
206+
186207
protected:
187208
virtual size_t _currentClientWrite(const char* b, size_t l) {
188209
return _currentClient->write(b, l);
@@ -193,7 +214,7 @@ class HTTPServer {
193214
void _addRequestHandler(RequestHandler* handler);
194215
void _handleRequest();
195216
void _finalizeResponse();
196-
bool _parseRequest(WiFiClient* client);
217+
ClientFuture _parseRequest(WiFiClient* client);
197218
void _parseArguments(String data);
198219
static String _responseCodeToString(int code);
199220
bool _parseForm(WiFiClient* client, String boundary, uint32_t len);
@@ -249,4 +270,6 @@ class HTTPServer {
249270
String _snonce; // Store noance and opaque for future comparison
250271
String _sopaque;
251272
String _srealm; // Store the Auth realm between Calls
273+
274+
HookFunction _hook;
252275
};

libraries/WebServer/src/Parsing.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ static char* readBytesWithTimeout(WiFiClient* client, size_t maxLength, size_t&
7878
return buf;
7979
}
8080

81-
bool HTTPServer::_parseRequest(WiFiClient* client) {
81+
HTTPServer::ClientFuture HTTPServer::_parseRequest(WiFiClient* client) {
8282
// Read the first line of HTTP request
8383
String req = client->readStringUntil('\r');
8484
client->readStringUntil('\n');
@@ -93,7 +93,7 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
9393
int addr_end = req.indexOf(' ', addr_start + 1);
9494
if (addr_start == -1 || addr_end == -1) {
9595
log_e("Invalid request: %s", req.c_str());
96-
return false;
96+
return CLIENT_MUST_STOP;
9797
}
9898

9999
String methodStr = req.substring(0, addr_start);
@@ -110,6 +110,13 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
110110
_chunked = false;
111111
_clientContentLength = 0; // not known yet, or invalid
112112

113+
if (_hook) {
114+
auto whatNow = _hook(methodStr, url, client, mime::getContentType);
115+
if (whatNow != CLIENT_REQUEST_CAN_CONTINUE) {
116+
return whatNow;
117+
}
118+
}
119+
113120
HTTPMethod method = HTTP_ANY;
114121
size_t num_methods = sizeof(_http_method_str) / sizeof(const char *);
115122
for (size_t i = 0; i < num_methods; i++) {
@@ -120,7 +127,7 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
120127
}
121128
if (method == HTTP_ANY) {
122129
log_e("Unknown HTTP Method: %s", methodStr.c_str());
123-
return false;
130+
return CLIENT_MUST_STOP;
124131
}
125132
_currentMethod = method;
126133

@@ -186,7 +193,7 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
186193
char* plainBuf = readBytesWithTimeout(client, _clientContentLength, plainLength, HTTP_MAX_POST_WAIT);
187194
if ((int)plainLength < (int)_clientContentLength) {
188195
free(plainBuf);
189-
return false;
196+
return CLIENT_MUST_STOP;
190197
}
191198
if (_clientContentLength > 0) {
192199
if (isEncoded) {
@@ -214,7 +221,7 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
214221
// it IS a form
215222
_parseArguments(searchStr);
216223
if (!_parseForm(client, boundaryStr, _clientContentLength)) {
217-
return false;
224+
return CLIENT_MUST_STOP;
218225
}
219226
}
220227
} else {
@@ -249,7 +256,7 @@ bool HTTPServer::_parseRequest(WiFiClient* client) {
249256
log_v("Request: %s", url.c_str());
250257
log_v(" Arguments: %s", searchStr.c_str());
251258

252-
return true;
259+
return CLIENT_REQUEST_CAN_CONTINUE;
253260
}
254261

255262
bool HTTPServer::_collectHeader(const char* headerName, const char* headerValue) {

0 commit comments

Comments
 (0)