// Do not remove the include below
#include "https_server.h"
using namespace httpsserver;
#define HEADER_USERNAME "X-USERNAME"
#define HEADER_GROUP "X-GROUP"
/**
* This callback will be linked to the web server root and answer with
* a small HTML page that shows the uptime of the esp.
*
* Note that the headers may be transmitted as soon as the first data output
* is sent. So calls to setStatusCode, setStatusText or setHeader should be
* made before the print functions are called.
*
* HTTPResponse implements Print, so it can be used very similar to other
* Arduino classes like Serial/...
*/
void testCallback(HTTPRequest * req, HTTPResponse * res) {
res->setStatusCode(200);
res->setStatusText("OK");
res->setHeader("Content-Type", "text/html; charset=utf8");
res->println("");
res->println("");
res->println("");
// test if connection is encrypted
res->println(req->isSecure() ? "HTTPS Server on ESP32" : "HTTP Server on ESP32");
res->println("");
res->println("");
res->println(req->isSecure() ? "
Hello HTTPS world!
" : "
Hello HTTP world!
");
res->println("
... from your ESP32
");
// The image resource is created in the awesomeCallback some lines below
res->println("");
res->println("");
res->println("");
res->print("
System has been up for ");
res->print((int)(millis()/1000), DEC);
res->println(" seconds.
");
res->println("");
res->println("");
}
/**
* The URL Param Callback demonstrates the usage of placeholders in the URL.
*
* This callback function is mapped to "param/ * / *" (ignore the spaces, they are required
* because of the C comment syntax).
*
* The placeholder values can be accessed through HTTPRequest::getParams. They are indexed
* beginning from 0.
*/
void urlParamCallback(HTTPRequest * req, HTTPResponse * res) {
// Get access to the parameters
ResourceParameters * params = req->getParams();
// Set a simple content type
res->setHeader("Content-Type", "text/plain");
// Print the first parameter
res->print("Parameter 1: ");
res->printStd(params->getUrlParameter(0));
// Print the second parameter
res->print("\nParameter 2: ");
res->printStd(params->getUrlParameter(1));
}
/**
* This callback responds with an SVG image to a GET request. The icon is the awesome smiley.
*
* If the color request parameter is set (so the URL is like awesome.svg?color=fede58), the
* background of our awesome face is changed.
*/
void awesomeCallback(HTTPRequest * req, HTTPResponse * res) {
// Get access to the parameters
ResourceParameters * params = req->getParams();
// Set SVG content type
res->setHeader("Content-Type", "image/svg+xml");
// Check if there is a suitabel fill color in the parameter:
std::string fillColor = "fede58";
// Get request parameter
std::string colorParamName = "color";
if (params->isRequestParameterSet(colorParamName)) {
std::string requestColor = params->getRequestParameter(colorParamName);
if (requestColor.length()==6) {
bool colorOk = true;
for(int i = 1; i < 6 && colorOk; i++) {
if (!(
(requestColor[i]>='0' && requestColor[i]<='9' ) ||
(requestColor[i]>='a' && requestColor[i]<='f' )
)) {
colorOk = false;
}
}
if (colorOk) {
fillColor = requestColor;
}
}
}
// Print the data
// Source: https://commons.wikimedia.org/wiki/File:718smiley.svg
res->print("");
res->print("");
}
/**
* This callback is configured to match all OPTIONS requests (see pattern configuration below)
*
* This allows to define headers there that are required to allow cross-domain-xhr-requests,
* which enabled a REST-API that can be used on the esp32, while the WebInterface is hosted
* somewhere else (on a host with more storage space to provide huge JS libraries etc.)
*
* An example use case would be an IoT dashboard that connects to a bunch of local esp32s,
* which provide data via their REST-interfaces that is aggregated in the dashboard.
*/
void corsCallback(HTTPRequest * req, HTTPResponse * res) {
res->setHeader("Access-Control-Allow-Methods", "HEAD,GET,POST,DELETE,PUT,OPTIONS");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Headers", "*");
}
/**
* This callback simply copies the requests body into the response body.
*
* It can be used to test POST and PUT functionality and is configured to reply to
* POST /echo and PUT /echo
*/
void echoCallback(HTTPRequest * req, HTTPResponse * res) {
res->setHeader("Content-Type","text/plain");
byte buffer[256];
while(!(req->requestComplete())) {
size_t s = req->readBytes(buffer, 256);
res->write(buffer, s);
}
}
/**
* This callback belongs to the authentication example. It is the user/password
* protected page visible as /internal and has a user-specific greeting.
*/
void internalCallback(HTTPRequest * req, HTTPResponse * res) {
res->setStatusCode(200);
res->setStatusText("OK");
res->setHeader("Content-Type", "text/html; charset=utf8");
res->println("");
res->println("");
res->println("");
res->println("Internal Area");
res->println("");
res->println("");
res->print("
Hello ");
// We can safely use the header value, this area is only accessible if it's set.
res->printStd(req->getHeader(HEADER_USERNAME));
res->print("!
");
res->println("
Welcome to the internal area. Congratulations to successfully entering your password!
");
// The "admin area" will only be shown if the correct group has been assigned in the authenticationMiddleware
if (req->getHeader(HEADER_GROUP) == "ADMIN") {
res->println("
");
res->println("
You are an administrator
");
res->println("
If this was more than a simple example, you could do crazy things here.
");
res->println("");
res->println("");
}
/**
* This resource callback also has limited access, but it manages it manually instead of letting the middleware do all the stuff.
*/
void internalAdminCallback(HTTPRequest * req, HTTPResponse * res) {
res->setHeader("Content-Type", "text/html; charset=utf8");
std::string header = "Secret Admin Page
Secret Admin Page
";
std::string footer = "";
// Checking permissions can not only be done centrally in the middleware function but also in the actual request handler.
// This would be handy if you provide an API with lists of resources, but access rights are defined object-based.
if (req->getHeader(HEADER_GROUP) == "ADMIN") {
res->setStatusCode(200);
res->setStatusText("OK");
res->printStd(header);
res->println("
");
}
res->printStd(footer);
}
/**
* This callback will be registered as default callback. The default callback is used
* if no other node matches the request.
*
* Again, another content type is shown (json).
*/
void notfoundCallback(HTTPRequest * req, HTTPResponse * res) {
// Discard the request body, as the 404-handler may also be used for put and post actions
req->discardRequestBody();
res->setStatusCode(404);
res->setStatusText("Not found");
res->setHeader("Content-Type", "application/json");
res->print("{\"error\":\"not found\", \"code\":404}");
}
/**
* The loggingMiddleware is an example for a middleware function. It will be called for every
* request, but before the request is passed to the actual handler function. It may just do
* some generic functions like logging, but it may also modify the request and response directly.
*
* Additionally to the Request and Response parameters that are similar to the request handler
* function, it also gets a function pointer to a next() function. Only if next is called,
* the request handler function will be called. It is also possible to chain multiple middleware
* functions using this, handing over control step by step.
*
* Not calling the next() function will not handle the request even though it might be configured in
* a resource node. This allows functionality like access control.
*
* Make sure to place your code correctly before, after or around the next() call. In this example,
* we want to log the request method, the request url, login user name and the status code. The first
* two bits of information are available from the request, but the status code is only set after the
* response is finished and the username header is set by a middleware function later in the chain.
* So we have to place our logging call below the next() function.
*/
void loggingMiddleware(HTTPRequest * req, HTTPResponse * res, std::function next) {
next();
Serial.printf("loggingMiddleware: %3d\t%s\t%s\t%s\n",
res->getStatusCode(),
req->getMethod().c_str(),
req->getHeader(HEADER_USERNAME).length() > 0 ? req->getHeader(HEADER_USERNAME).c_str() : "NOBODY",
req->getRequestString().c_str());
}
/**
* The following middleware function is one of two functions dealing with access control. The
* authenticationMiddleware will interpret the HTTP Basic Auth header, check usernames and password,
* and if they are valid, set the X-USERNAME and X-GROUP header.
*
* If they are invalid, the X-USERNAME and X-GROUP header will be unset. This is important because
* otherwise the client may manipulate those internal headers.
*
* From then on, further middleware functions and the request handler functions will be able to just
* use req->getHeader("X-USERNAME") to find out if the user is logged in correctly.
*
* Furthermore, if the user supplies credentials and they are invalid, he will receive an 403 response
* without any other functions being called.
*/
void authenticationMiddleware(HTTPRequest * req, HTTPResponse * res, std::function next) {
// Unset both headers to discard any value from the client
req->setHeader(HEADER_USERNAME, "");
req->setHeader(HEADER_GROUP, "");
// Get login information from request
std::string reqUsername = req->getBasicAuthUser();
std::string reqPassword = req->getBasicAuthPassword();
// If the user entered login information, we will check it
if (reqUsername.length() + reqPassword.length() > 0) {
// _Very_ simple user database
bool authValid = true;
std::string group = "";
if (reqUsername == "admin" && reqPassword == "secret") {
group = "ADMIN";
} else if (reqUsername == "user" && reqPassword == "test") {
group = "USER";
} else {
authValid = false;
}
// If authentication was successful
if (authValid) {
// set custom headers and delegate control
req->setHeader(HEADER_USERNAME, reqUsername);
req->setHeader(HEADER_GROUP, group);
next();
} else {
// Display error page
res->setStatusCode(401);
res->setStatusText("Unauthorized");
res->setHeader("Content-Type", "text/plain");
// This should trigger the browser user/password dialog:
res->setHeader("WWW-Authenticate", "Basic realm=\"ESP32 privileged area\"");
res->println("401. Unauthorized (try admin/secret or user/test)");
}
} else {
// Otherwise just let the request pass through
next();
}
}
/**
* This function plays together with the authenticationMiddleware. While the first function checks the
* username/password combination and stores it in the request, this function makes use of this information
* to allow or deny access.
*
* This example only prevents unauthorized access to every ResourceNode stored under an /internal/... path.
*/
void authorizationMiddleware(HTTPRequest * req, HTTPResponse * res, std::function next) {
std::string username = req->getHeader(HEADER_USERNAME);
// Check that only logged-in users may get to the internal area (All URLs starting with /internal
// Only a simple example, more complicated configuration is up to you.
if (username == "" && req->getRequestString().substr(0,9) == "/internal") {
// Same as above
res->setStatusCode(401);
res->setStatusText("Unauthorized");
res->setHeader("Content-Type", "text/plain");
res->setHeader("WWW-Authenticate", "Basic realm=\"ESP32 privileged area\"");
res->println("401. Unauthorized (try admin/secret or user/test)");
} else {
// Everything else will be allowed.
next();
}
}
//The setup function is called once at startup of the sketch
void setup()
{
Serial.begin(115200);
Serial.println("setup()");
// Setup wifi. We use the configuration stored at data/wifi/wifi.h
// If you don't have that file, make sure to copy the wifi.example.h,
// rename it and also configure your wifi settings there.
Serial.print("Connecting WiFi");
WiFi.begin(WIFI_SSID, WIFI_PSK);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.print(" connected. IP=");
Serial.println(WiFi.localIP());
// Setup the server as a separate task.
//
// Important note: If the server is launched as a different task, it has its own
// stack. This means that we cannot use globally instantiated Objects there.
// -> Make sure to create Server, ResourceNodes, etc. in the function where they
// are used (serverTask() in this case).
// Another alternative would be to instantiate the objects on the heap. This is
// especially important for data that should be accessed by the main thread and
// the server.
Serial.println("Creating server task... ");
// If stack canary errors occur, try to increase the stack size (3rd parameter)
// or to put as much stuff as possible onto the heap (ResourceNodes etc)
// 4096 byte _should_ suffice for the https server only, but with the http server
// running in the same task, that's not enough for stable operation, so we use
// a bit more here.
xTaskCreatePinnedToCore(serverTask, "https443", 6144, NULL, 1, NULL, ARDUINO_RUNNING_CORE);
Serial.println("Beginning to loop()...");
}
// The loop function is called in an endless loop
void loop() {
// Use your normal loop without thinking of the server in the background
// Delay for about five seconds and print some message on the Serial console.
delay(5 * 1000);
// We use this loop only to show memory usage for debugging purposes
uint32_t freeheap = ESP.getFreeHeap();
Serial.printf("Free Heap: %9d \n", freeheap);
}
/**
* As mentioned above, the serverTask method contains the code to start the server.
*
* The infinite loop in the function is the equivalent for the loop() function of a
* regular Arduino sketch.
*/
void serverTask(void *params) {
Serial.println("Configuring Server...");
// Define the certificate that should be used
// See files in tools/cert on how to create the headers containing the certificate.
// Because they are just byte arrays, it would also be possible to store and load them from
// non-volatile memory after creating them on the fly when the device is launched for the
// first time.
SSLCert cert = SSLCert(
example_crt_DER, example_crt_DER_len,
example_key_DER, example_key_DER_len
);
// The faviconCallback now is assigned to the /favicon.ico node, when accessed by GET
// This means, it can be accessed by opening https://myesp/favicon.ico in all
// web browsers. Most browser fetch this file in background every time a new webserver
// is used to show the icon in the tab of that website.
ResourceNode * faviconNode = new ResourceNode("/favicon.ico", "GET", &faviconCallback);
// The awesomeCallback is very similar to the favicon.
ResourceNode * awesomeNode = new ResourceNode("/images/awesome.svg", "GET", &awesomeCallback);
// A simple callback showing URL parameters. Every asterisk (*) is a placeholder value
// So, the following URL has two placeholders that have to be filled.
// This is especially useful for REST-APIs where you want to represent an object ID in the
// url. Placeholders are arbitrary strings, but may be converted to integers (Error handling
// is up to the callback, eg. returning 404 if there is no suitable resource for that placeholder
// value)
ResourceNode * urlParamNode = new ResourceNode("/param/*/*", "GET", &urlParamCallback);
// The echoCallback is configured on the path /echo for POST and PUT requests. It just copies request
// body to response body. To enable it for both methods, two nodes have to be created:
ResourceNode * echoNodePost = new ResourceNode("/echo", "POST", &echoCallback);
ResourceNode * echoNodePut = new ResourceNode("/echo", "PUT", &echoCallback);
// The root node (on GET /) will be called when no directory on the server is specified in
// the request, so this node can be accessed through https://myesp/
ResourceNode * rootNode = new ResourceNode("/", "GET", &testCallback);
// As mentioned above, we want to answer all OPTIONS requests with a response that allows
// cross-domain XHR. To do so, we bind the corsCallback to match all options request
// (we can exploit the asterisk functionality for this. The callback is not required to
// process the parameters in any way.)
// Note the difference to the "/" in the rootNode above - "/" matches ONLY that specific
// resource, while slash and asterisk is more or less provides a catch all behavior
ResourceNode * corsNode = new ResourceNode("/*", "OPTIONS", &corsCallback);
// Those two nodes belong to the middleware authentication and authorization example.
// They are protected if no user/password is given (see authenticationMiddleware and authorizationMiddleware
// for details.
ResourceNode * internalNode = new ResourceNode("/internal", "GET", &internalCallback);
ResourceNode * internalAdminNode = new ResourceNode("/internal/admin", "GET", &internalAdminCallback);
// The not found node will be used when no other node matches, and it's configured as
// defaultNode in the server.
// Note: Despite resource and method string have to be specified when a node is created,
// they are ignored for the default node. However, this makes it possible to register another
// node as default node as well.
ResourceNode * notFoundNode = new ResourceNode("/", "GET", ¬foundCallback);
// Create the SSL server. The constructor takes some optional parameters, eg. to specify the TCP
// port that should be used. However, defining a certificate is mandatory.
HTTPSServer secureServer = HTTPSServer(&cert);
// We also create a default HTTP server without encryption on port 80
HTTPServer insecureServer = HTTPServer();
// We put references to both servers in an array so we can configure them in a loop (as we are lazy).
// Note that you can use the same ResourceNode on multiple servers!
HTTPServer * serverList[] = {&secureServer, &insecureServer};
for(int i = 0; i < 2; i++) {
HTTPServer * server = serverList[i];
// Register the nodes that have been configured on the web server.
server->setDefaultNode(notFoundNode);
server->registerNode(rootNode);
server->registerNode(faviconNode);
server->registerNode(awesomeNode);
server->registerNode(urlParamNode);
server->registerNode(echoNodePost);
server->registerNode(echoNodePut);
server->registerNode(corsNode);
server->registerNode(internalNode);
server->registerNode(internalAdminNode);
// Add a default header to the server that will be added to every response. In this example, we
// use it only for adding the server name, but it could also be used to add CORS-headers to every response
server->setDefaultHeader("Server", "esp32-http-server");
// Add all middleware functions to the server. Order is important!
server->addMiddleware(&loggingMiddleware);
server->addMiddleware(&authenticationMiddleware);
server->addMiddleware(&authorizationMiddleware);
}
// The web server can be start()ed and stop()ed. When it's stopped, it will close its server port and
// all open connections and free the resources. Theoretically, it should be possible to run multiple
// web servers in parallel, however, there might be some restrictions im memory.
Serial.println("Starting HTTP Server...");
insecureServer.start();
Serial.println("Starting HTTPS Server...");
secureServer.start();
// We check whether the server did come up correctly (it might fail if there aren't enough free resources)
if (insecureServer.isRunning() && secureServer.isRunning()) {
// If the server is started, we go into our task's loop
Serial.println("Servers started.");
while(1) {
// Run the server loop.
// This loop function accepts new clients on the server socket if there are connection slots available
// (see the optional parameter maxConnections on the HTTPSServer constructor).
// It also frees resources by connections that have been closed by either the client or the application.
// Finally, it calls the loop() function of each active HTTPSConnection() to process incoming requests,
// which will finally trigger calls to the request handler callbacks that have been configured through
// the ResourceNodes.
insecureServer.loop();
secureServer.loop();
delay(1);
}
} else {
// For the sake of this example, we just restart the ESP in case of failure and hope it's getting better
// next time.
Serial.println("Starting Server FAILED! Restart in 10 seconds");
delay(10000);
ESP.restart();
}
}