Skip to content

Commit

Permalink
Arduino WebServer improvements
Browse files Browse the repository at this point in the history
- Add Middleware support (aka Expressif) with some built-in middlewares
- Add ability to collect all incoming request headers
  • Loading branch information
mathieucarbou committed Aug 17, 2024
1 parent def319a commit 0bc70ee
Show file tree
Hide file tree
Showing 9 changed files with 729 additions and 85 deletions.
54 changes: 54 additions & 0 deletions libraries/WebServer/examples/Middleware/Middleware.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#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(Serial);
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());

cors.origin("http://192.168.4.1");
cors.methods("POST, GET, OPTIONS, DELETE");
cors.headers("X-Custom-Header");
cors.allowCredentials(false);
cors.maxAge(600);

auth.authenticate("admin", "admin");

server
.on(
"/",
[]() {
server.send(200, "text/plain", "Home");
}
)
.addMiddleware(&logger)
.addMiddleware(&cors)
.addMiddleware(&auth);

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
}
109 changes: 109 additions & 0 deletions libraries/WebServer/examples/Middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
This example shows how to load all request headers and use middleware.

### CORS Middleware

```bash
❯ curl -i -X OPTIONS http://192.168.4.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
```

Output of logger middleware:

```
* Connection from 192.168.4.2:57597
< OPTIONS / HTTP/1.1
< Host: 192.168.4.1
< User-Agent: curl/8.9.1
< Accept: */*
<
* Processed!
> 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
>
```

### Authentication Middleware

```bash
❯ curl -i -X GET http://192.168.4.1
HTTP/1.1 401 Unauthorized
Content-Type: text/html
WWW-Authenticate: Basic realm=""
Content-Length: 0
Connection: close
```

Output of logger middleware:

```
* Connection from 192.168.4.2:57705
< GET / HTTP/1.1
< Host: 192.168.4.1
< User-Agent: curl/8.9.1
< Accept: */*
<
* Processed!
> HTTP/1.HTTP/1.1 401 Unauthorized
> Content-Type: text/html
> WWW-Authenticate: Basic realm=""
> Content-Length: 0
> Connection: close
>
```

Sending auth...

```bash
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying 192.168.4.1:80...
* Connected to 192.168.4.1 (192.168.4.1) port 80
* Server auth using Basic with user 'admin'
> GET / HTTP/1.1
> Host: 192.168.4.1
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/8.9.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 4
< Connection: close
<
* shutting down connection #0
Home

```

Output of logger middleware:

```
* Connection from 192.168.4.2:62099
< GET / HTTP/1.1
< Authorization: Basic YWRtaW46YWRtaW4=
< Host: 192.168.4.1
< User-Agent: curl/8.9.1
< Accept: */*
<
* Processed!
> HTTP/1.HTTP/1.1 200 OK
> Content-Type: text/plain
> Content-Length: 4
> Connection: close
>
```
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 @@
{
"targets": {
"esp32h2": false
}
}
208 changes: 208 additions & 0 deletions libraries/WebServer/src/Middlewares.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#ifndef MIDDLEWARES_H
#define MIDDLEWARES_H

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

#include <assert.h>

// curl-like logging middleware
class LoggingMiddleware : public Middleware {
private:
Stream *_out;

public:
explicit LoggingMiddleware(Stream &out) : _out(&out) {}

bool run(WebServer &server, Middleware::Callback next) override {
_out->print(F("* Connection from "));
_out->print(server.client().remoteIP().toString());
_out->print(F(":"));
_out->println(server.client().remotePort());

_out->print(F("< "));
HTTPMethod method = server.method();
if (method == HTTP_ANY) {
_out->print(F("HTTP_ANY"));
} else {
_out->print(http_method_str((http_method)method));
}
_out->print(F(" "));
_out->print(server.uri());
_out->print(F(" "));
_out->println(server.version());

int n = server.headers();
for (int i = 0; i < n; i++) {
String v = server.header(i);
if (!v.isEmpty()) {
// because these 2 are always there, eventually empty: "Authorization", "If-None-Match"
_out->print(F("< "));
_out->print(server.headerName(i));
_out->print(F(": "));
_out->println(server.header(i));
}
}

_out->println(F("<"));

bool ret = next();

if (ret) {
_out->println(F("* Processed!"));

_out->print(F("> "));
_out->print(F("HTTP/1."));
_out->print(server.version());
_out->print(F(" "));
_out->print(server.responseCode());
_out->print(F(" "));
_out->println(WebServer::responseCodeToString(server.responseCode()));

n = server.responseHeaders();
for (int i = 0; i < n; i++) {
_out->print(F("> "));
_out->print(server.responseHeaderName(i));
_out->print(F(": "));
_out->println(server.responseHeader(i));
}

_out->println(F(">"));

} else {
_out->println(F("* Not processed!"));
}

return ret;
}
};

class AuthenticationMiddleware : public Middleware {
private:
// authenticate state
// 0: not authenticated
// 1: callback
// 2: username/password
// 3: sha1
int _auth = 0;
WebServer::THandlerFunctionAuthCheck _fn;
String _username;
String _password;
String _sha1;

// authenticate request
HTTPAuthMethod _mode = BASIC_AUTH;
String _realm;
String _authFailMsg;

public:
AuthenticationMiddleware &authenticate(WebServer::THandlerFunctionAuthCheck fn) {
assert(fn);
_fn = fn;
_auth = 1;
return *this;
}

AuthenticationMiddleware &authenticate(const char *username, const char *password) {
if (strlen(username) == 0 || strlen(password) == 0) {
_auth = 0;
return *this;
} else {
_username = username;
_password = password;
_auth = 2;
return *this;
}
}

AuthenticationMiddleware &authenticateBasicSHA1(const char *username, const char *sha1AsBase64orHex) {
if (strlen(username) == 0 || strlen(sha1AsBase64orHex) == 0) {
_auth = 0;
return *this;
}
_username = username;
_sha1 = sha1AsBase64orHex;
_auth = 3;
return *this;
}

bool run(WebServer &server, Middleware::Callback next) override {
switch (_auth) {
case 1:
if (server.authenticate(_fn)) {
return next();
} else {
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
return true;
}

case 2:
if (server.authenticate(_username.c_str(), _password.c_str())) {
return next();
} else {
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
return true;
}

case 3:
if (server.authenticate(_username.c_str(), _sha1.c_str())) {
return next();
} else {
server.requestAuthentication(_mode, _realm.c_str(), _authFailMsg);
return true;
}

default: return next();
}
}
};

class CorsMiddleware : public Middleware {
private:
String _origin = F("*");
String _methods = F("*");
String _headers = F("*");
bool _credentials = true;
uint32_t _maxAge = 86400;

public:
CorsMiddleware &origin(const char *origin) {
_origin = origin;
return *this;
}

CorsMiddleware &methods(const char *methods) {
_methods = methods;
return *this;
}

CorsMiddleware &headers(const char *headers) {
_headers = headers;
return *this;
}

CorsMiddleware &allowCredentials(bool credentials) {
_credentials = credentials;
return *this;
}

CorsMiddleware &maxAge(uint32_t seconds) {
_maxAge = seconds;
return *this;
}

bool run(WebServer &server, Middleware::Callback next) override {
if (server.method() == HTTP_OPTIONS) {
server.sendHeader(F("Access-Control-Allow-Origin"), _origin.c_str());
server.sendHeader(F("Access-Control-Allow-Methods"), _methods.c_str());
server.sendHeader(F("Access-Control-Allow-Headers"), _headers.c_str());
server.sendHeader(F("Access-Control-Allow-Credentials"), _credentials ? F("true") : F("false"));
server.sendHeader(F("Access-Control-Max-Age"), String(_maxAge).c_str());
server.send(200);
return true;
}
return next();
}
};

#endif
Loading

0 comments on commit 0bc70ee

Please sign in to comment.