diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ba97b..b75efe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ Breaking changes: – +## [v1.1.0](https://github.com/fhessel/esp32_https_server/releases/tag/v1.0.0) + +New functionality: + +* Add examples to support WT32_ETH01 using LAN8720 + +Bug fixes: + +* Fix compile error for using `hwcrypto/sha.h` + +========================================================= + ## [v1.0.0](https://github.com/fhessel/esp32_https_server/releases/tag/v1.0.0) New functionality: diff --git a/README.md b/README.md index 33773dd..88d2f04 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # ESP32 HTTPS Server -![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/fhessel/esp32_https_server?label=Version&sort=semver) ![Build Examples](https://github.com/fhessel/esp32_https_server/workflows/Build%20Examples/badge.svg?branch=master) - This repository contains an HTTPS server library that can be used with the [ESP32 Arduino Core](https://github.com/espressif/arduino-esp32). It supports HTTP as well. ## Features @@ -73,19 +71,18 @@ git clone https://github.com/fhessel/esp32_https_server.git > **Note:** To run the examples (except for the _Self-Signed-Certificates_ example), you need to execute the script extras/create_cert.sh first (see [Issue #26](https://github.com/fhessel/esp32_https_server/issues/26) for Windows). This script will create a simple CA to sign certificates that are used with the examples. Some notes on the usage can be found in the extras/README.md file. -You will find several examples showing how you can use the library (roughly ordered by complexity): +You will find several examples showing how you can use the library: - [Static-Page](examples/Static-Page/Static-Page.ino): Short example showing how to serve some static resources with the server. You should start with this sketch and get familiar with it before having a look at the more complex examples. - [Parameters](examples/Parameters/Parameters.ino): Shows how you can access request parameters (the part after the question mark in the URL) or parameters in dynamic URLs (like /led/1, /led/2, ...) -- [Parameter-Validation](examples/Parameter-Validation/Parameter-Validation.ino): Shows how you can integrate validator functions to do formal checks on parameters in your URL. - [Put-Post-Echo](examples/Put-Post-Echo/Put-Post-Echo.ino): Implements a simple echo service for PUT and POST requests that returns the request body as response body. Also shows how to differentiate between multiple HTTP methods for the same URL. - [HTTPS-and-HTTP](examples/HTTPS-and-HTTP/HTTPS-and-HTTP.ino): Shows how to serve resources via HTTP and HTTPS in parallel and how to check if the user is using a secure connection during request handling -- [HTML-Forms](examples/HTML-Forms/HTML-Forms.ino): Shows how to use body parsers to handle requests created from HTML forms (access text field contents, handle file upload, etc.). -- [Async-Server](examples/Async-Server/Async-Server.ino): Like the Static-Page example, but the server runs in a separate task on the ESP32, so you do not need to call the loop() function in your main sketch. -- [Self-Signed-Certificate](examples/Self-Signed-Certificate/Self-Signed-Certificate.ino): Shows how to generate a self-signed certificate on the fly on the ESP when the sketch starts. You do not need to run `create_cert.sh` to use this example. - [Middleware](examples/Middleware/Middleware.ino): Shows how to use the middleware API for logging. Middleware functions are defined very similar to webservers like Express. - [Authentication](examples/Authentication/Authentication.ino): Implements a chain of two middleware functions to handle authentication and authorization using HTTP Basic Auth. +- [Async-Server](examples/Async-Server/Async-Server.ino): Like the Static-Page example, but the server runs in a separate task on the ESP32, so you do not need to call the loop() function in your main sketch. - [Websocket-Chat](examples/Websocket-Chat/Websocket-Chat.ino): Provides a browser-based chat built on top of websockets. **Note:** Websockets are still under development! +- [Parameter-Validation](examples/Parameter-Validation/Parameter-Validation.ino): Shows how you can integrate validator functions to do formal checks on parameters in your URL. +- [Self-Signed-Certificate](examples/Self-Signed-Certificate/Self-Signed-Certificate.ino): Shows how to generate a self-signed certificate on the fly on the ESP when the sketch starts. You do not need to run `create_cert.sh` to use this example. - [REST-API](examples/REST-API/REST-API.ino): Uses [ArduinoJSON](https://arduinojson.org/) and [SPIFFS file upload](https://github.com/me-no-dev/arduino-esp32fs-plugin) to serve a small web interface that provides a REST API. If you encounter error messages that cert.h or private\_key.h are missing when running an example, make sure to run create\_cert.sh first (see Setup Instructions). diff --git a/examples/Async-Server/Async-Server.ino b/examples/Async-Server/Async-Server.ino index d987a51..8fed51d 100644 --- a/examples/Async-Server/Async-Server.ino +++ b/examples/Async-Server/Async-Server.ino @@ -1,31 +1,31 @@ /** - * Example for the ESP32 HTTP(S) Webserver - * - * IMPORTANT NOTE: - * To run this script, your need to - * 1) Enter your WiFi SSID and PSK below this comment - * 2) Make sure to have certificate data available. You will find a - * shell script and instructions to do so in the library folder - * under extras/ - * - * This script will install an HTTPS Server on your ESP32 with the following - * functionalities: - * - Show simple page on web server root - * - 404 for everything else - * The server will be run in a separate task, so that you can do your own stuff - * in the loop() function. - * Everything else is just like the Static-Page example - */ + Example for the ESP32 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Enter your WiFi SSID and PSK below this comment + 2) Make sure to have certificate data available. You will find a + shell script and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your ESP32 with the following + functionalities: + - Show simple page on web server root + - 404 for everything else + The server will be run in a separate task, so that you can do your own stuff + in the loop() function. + Everything else is just like the Static-Page example +*/ // TODO: Configure your WiFi here -#define WIFI_SSID "" -#define WIFI_PSK "" +#define WIFI_SSID "your_ssid" +#define WIFI_PSK "12345678" /** Check if we have multiple cores */ #if CONFIG_FREERTOS_UNICORE -#define ARDUINO_RUNNING_CORE 0 + #define ARDUINO_RUNNING_CORE 0 #else -#define ARDUINO_RUNNING_CORE 1 + #define ARDUINO_RUNNING_CORE 1 #endif // Include certificate data (see note above) @@ -46,53 +46,15 @@ using namespace httpsserver; // Create an SSL certificate object from the files included above SSLCert cert = SSLCert( - example_crt_DER, example_crt_DER_len, - example_key_DER, example_key_DER_len -); + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); // Create an SSL-enabled server that uses the certificate HTTPSServer secureServer = HTTPSServer(&cert); -// Declare some handler functions for the various URLs on the server -void handleRoot(HTTPRequest * req, HTTPResponse * res); -void handle404(HTTPRequest * req, HTTPResponse * res); - -// We declare a function that will be the entry-point for the task that is going to be -// created. -void serverTask(void *params); - -void setup() { - // For logging - Serial.begin(115200); - - // Connect to WiFi - Serial.println("Setting up WiFi"); - WiFi.begin(WIFI_SSID, WIFI_PSK); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); - } - Serial.print("Connected. IP="); - Serial.println(WiFi.localIP()); - - // Setup the server as a separate task. - Serial.println("Creating server task... "); - // We pass: - // serverTask - the function that should be run as separate task - // "https443" - a name for the task (mainly used for logging) - // 6144 - stack size in byte. If you want up to four clients, you should - // not go below 6kB. If your stack is too small, you will encounter - // Panic and stack canary exceptions, usually during the call to - // SSL_accept. - xTaskCreatePinnedToCore(serverTask, "https443", 6144, NULL, 1, NULL, ARDUINO_RUNNING_CORE); -} - -void loop() { - Serial.println("loop()"); - delay(5000); -} - -void serverTask(void *params) { +void serverTask(void *params) +{ // In the separate task we first do everything that we would have done in the // setup() function, if we would run the server synchronously. @@ -112,11 +74,14 @@ void serverTask(void *params) { Serial.println("Starting server..."); secureServer.start(); - if (secureServer.isRunning()) { + + if (secureServer.isRunning()) + { Serial.println("Server ready."); // "loop()" function of the separate task - while(true) { + while (true) + { // This call will let the server do its work secureServer.loop(); @@ -126,7 +91,8 @@ void serverTask(void *params) { } } -void handleRoot(HTTPRequest * req, HTTPResponse * res) { +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ // Status code is 200 OK by default. // We want to deliver a simple HTML page, so we send a corresponding content type: res->setHeader("Content-Type", "text/html"); @@ -140,13 +106,14 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) { res->println("

Hello World!

"); res->print("

Your server is running for "); // A bit of dynamic data: Show the uptime - res->print((int)(millis()/1000), DEC); + res->print((int)(millis() / 1000), DEC); res->println(" seconds.

"); res->println(""); res->println(""); } -void handle404(HTTPRequest * req, HTTPResponse * res) { +void handle404(HTTPRequest * req, HTTPResponse * res) +{ // Discard request body, if we received any // We do this, as this is the default node and may also server POST/PUT requests req->discardRequestBody(); @@ -165,3 +132,44 @@ void handle404(HTTPRequest * req, HTTPResponse * res) { res->println("

404 Not Found

The requested resource was not found on this server.

"); res->println(""); } + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Async_Server on "); Serial.println(ARDUINO_BOARD); + + // Connect to WiFi + Serial.println("Setting up WiFi"); + WiFi.begin(WIFI_SSID, WIFI_PSK); + + while (WiFi.status() != WL_CONNECTED) + { + Serial.print("."); + delay(500); + } + + Serial.print("Connected. IP="); + Serial.println(WiFi.localIP()); + + // Setup the server as a separate task. + Serial.println("Creating server task... "); + // We pass: + // serverTask - the function that should be run as separate task + // "https443" - a name for the task (mainly used for logging) + // 6144 - stack size in byte. If you want up to four clients, you should + // not go below 6kB. If your stack is too small, you will encounter + // Panic and stack canary exceptions, usually during the call to + // SSL_accept. + xTaskCreatePinnedToCore(serverTask, "https443", 6144, NULL, 1, NULL, ARDUINO_RUNNING_CORE); +} + +void loop() +{ + Serial.println("loop()"); + delay(5000); +} diff --git a/examples/Async-Server/cert.h b/examples/Async-Server/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/Async-Server/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/Async-Server/private_key.h b/examples/Async-Server/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/Async-Server/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/Authentication/Authentication.ino b/examples/Authentication/Authentication.ino index 0a1cb2d..a66506d 100644 --- a/examples/Authentication/Authentication.ino +++ b/examples/Authentication/Authentication.ino @@ -1,28 +1,28 @@ /** - * Example for the ESP32 HTTP(S) Webserver - * - * IMPORTANT NOTE: - * To run this script, your need to - * 1) Enter your WiFi SSID and PSK below this comment - * 2) Make sure to have certificate data available. You will find a - * shell script and instructions to do so in the library folder - * under extras/ - * - * This script will install an HTTPS Server on your ESP32 with the following - * functionalities: - * - Show simple page on web server root - * - Provide some "internal pages" that are protected by the server - * - Run a middleware that authenticates the user - * - Run a middleware that provides access control - * - 404 for everything else - * Authentication is done using HTTP Basic Auth, which is supported by the webserver, - * so you don't have to care about retrieving the login information from request - * headers. - */ + Example for the ESP32 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Enter your WiFi SSID and PSK below this comment + 2) Make sure to have certificate data available. You will find a + shell script and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your ESP32 with the following + functionalities: + - Show simple page on web server root + - Provide some "internal pages" that are protected by the server + - Run a middleware that authenticates the user + - Run a middleware that provides access control + - 404 for everything else + Authentication is done using HTTP Basic Auth, which is supported by the webserver, + so you don't have to care about retrieving the login information from request + headers. +*/ // TODO: Configure your WiFi here -#define WIFI_SSID "" -#define WIFI_PSK "" +#define WIFI_SSID "your_ssid" +#define WIFI_PSK "12345678" // Include certificate data (see note above) #include "cert.h" @@ -51,21 +51,14 @@ using namespace httpsserver; // Create an SSL certificate object from the files included above SSLCert cert = SSLCert( - example_crt_DER, example_crt_DER_len, - example_key_DER, example_key_DER_len -); + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); // Create an SSL-enabled server that uses the certificate // The contstructor takes some more parameters, but we go for default values here. HTTPSServer secureServer = HTTPSServer(&cert); -// Declare some handler functions for the various URLs on the server -void handleRoot(HTTPRequest * req, HTTPResponse * res); -void handleInternalPage(HTTPRequest * req, HTTPResponse * res); -void handleAdminPage(HTTPRequest * req, HTTPResponse * res); -void handlePublicPage(HTTPRequest * req, HTTPResponse * res); -void handle404(HTTPRequest * req, HTTPResponse * res); - // Declare a middleware function. // Parameters: // req: Request data, can be used to access URL, HTTP Method, Headers, ... @@ -78,79 +71,23 @@ void handle404(HTTPRequest * req, HTTPResponse * res); // code 403 on the response to show that the user is not allowed to access a specific // resource. // For more details, see the definition below. -void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::function next); -void middlewareAuthorization(HTTPRequest * req, HTTPResponse * res, std::function next); - -void setup() { - // For logging - Serial.begin(115200); - - // Connect to WiFi - Serial.println("Setting up WiFi"); - WiFi.begin(WIFI_SSID, WIFI_PSK); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); - } - Serial.print("Connected. IP="); - Serial.println(WiFi.localIP()); - - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function - ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); - ResourceNode * nodeInternal = new ResourceNode("/internal", "GET", &handleInternalPage); - ResourceNode * nodeAdmin = new ResourceNode("/internal/admin", "GET", &handleAdminPage); - ResourceNode * nodePublic = new ResourceNode("/public", "GET", &handlePublicPage); - ResourceNode * node404 = new ResourceNode("", "GET", &handle404); - - // Add the nodes to the server - secureServer.registerNode(nodeRoot); - secureServer.registerNode(nodeInternal); - secureServer.registerNode(nodeAdmin); - secureServer.registerNode(nodePublic); - - // Add the 404 not found node to the server. - // The path is ignored for the default node. - secureServer.setDefaultNode(node404); - - // Add the middleware. These functions will be called globally for every request - // Note: The functions are called in the order they are added to the server. - // This means, we need to add the authentication middleware first, because the - // authorization middleware needs the headers that will be set by the authentication - // middleware (First we check the identity, then we see what the user is allowed to do) - secureServer.addMiddleware(&middlewareAuthentication); - secureServer.addMiddleware(&middlewareAuthorization); - Serial.println("Starting server..."); - secureServer.start(); - if (secureServer.isRunning()) { - Serial.println("Server ready."); - } -} +/** + The following middleware function is one of two functions dealing with access control. The + middlewareAuthentication() will interpret the HTTP Basic Auth header, check usernames and password, + and if they are valid, set the X-USERNAME and X-GROUP header. -void loop() { - // This call will let the server do its work - secureServer.loop(); + 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. - // Other code would go here... - delay(1); -} + Having that done, 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. -/** - * The following middleware function is one of two functions dealing with access control. The - * middlewareAuthentication() 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. - * - * Having that done, 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 401 response - * without any other functions being called. - */ -void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::function next) { + Furthermore, if the user supplies credentials and they are invalid, he will receive an 401 response + without any other functions being called. +*/ +void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::function next) +{ // Unset both headers to discard any value from the client // This prevents authentication bypass by a client that just sets X-USERNAME req->setHeader(HEADER_USERNAME, ""); @@ -165,21 +102,29 @@ void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::functi std::string reqPassword = req->getBasicAuthPassword(); // If the user entered login information, we will check it - if (reqUsername.length() > 0 && reqPassword.length() > 0) { + if (reqUsername.length() > 0 && reqPassword.length() > 0) + { // _Very_ simple hardcoded user database to check credentials and assign the group bool authValid = true; std::string group = ""; - if (reqUsername == "admin" && reqPassword == "secret") { + + if (reqUsername == "admin" && reqPassword == "secret") + { group = "ADMIN"; - } else if (reqUsername == "user" && reqPassword == "test") { + } + else if (reqUsername == "user" && reqPassword == "test") + { group = "USER"; - } else { + } + else + { authValid = false; } // If authentication was successful - if (authValid) { + if (authValid) + { // set custom headers and delegate control req->setHeader(HEADER_USERNAME, reqUsername); req->setHeader(HEADER_GROUP, group); @@ -187,7 +132,9 @@ void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::functi // The user tried to authenticate and was successful // -> We proceed with this request. next(); - } else { + } + else + { // Display error page res->setStatusCode(401); res->setStatusText("Unauthorized"); @@ -204,7 +151,9 @@ void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::functi // NO CALL TO next() here, as the authentication failed. // -> The code above did handle the request already. } - } else { + } + else + { // No attempt to authenticate // -> Let the request pass through by calling next() next(); @@ -212,19 +161,21 @@ void middlewareAuthentication(HTTPRequest * req, HTTPResponse * res, std::functi } /** - * This function plays together with the middlewareAuthentication(). 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 middlewareAuthorization(HTTPRequest * req, HTTPResponse * res, std::function next) { + This function plays together with the middlewareAuthentication(). 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 middlewareAuthorization(HTTPRequest * req, HTTPResponse * res, std::function next) +{ // Get the username (if any) 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") { + if (username == "" && req->getRequestString().substr(0, 9) == "/internal") + { // Same as the deny-part in middlewareAuthentication() res->setStatusCode(401); res->setStatusText("Unauthorized"); @@ -233,7 +184,9 @@ void middlewareAuthorization(HTTPRequest * req, HTTPResponse * res, std::functio res->println("401. Unauthorized (try admin/secret or user/test)"); // No call denies access to protected handler function. - } else { + } + else + { // Everything else will be allowed, so we call next() next(); } @@ -242,7 +195,8 @@ void middlewareAuthorization(HTTPRequest * req, HTTPResponse * res, std::functio // This is the internal page. It will greet the user with // a personalized message and - if the user is in the ADMIN group - // provide a link to the admin interface. -void handleInternalPage(HTTPRequest * req, HTTPResponse * res) { +void handleInternalPage(HTTPRequest * req, HTTPResponse * res) +{ // Header res->setStatusCode(200); res->setStatusText("OK"); @@ -266,7 +220,8 @@ void handleInternalPage(HTTPRequest * req, HTTPResponse * res) { res->println("

Welcome to the internal area. Congratulations on 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") { + if (req->getHeader(HEADER_GROUP) == "ADMIN") + { res->println("
"); res->println("

You are an administrator

"); res->println("

You are allowed to access the admin page:

"); @@ -280,7 +235,8 @@ void handleInternalPage(HTTPRequest * req, HTTPResponse * res) { res->println(""); } -void handleAdminPage(HTTPRequest * req, HTTPResponse * res) { +void handleAdminPage(HTTPRequest * req, HTTPResponse * res) +{ // Headers res->setHeader("Content-Type", "text/html; charset=utf8"); @@ -289,7 +245,8 @@ void handleAdminPage(HTTPRequest * req, HTTPResponse * res) { // 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") { + if (req->getHeader(HEADER_GROUP) == "ADMIN") + { res->setStatusCode(200); res->setStatusText("OK"); res->printStd(header); @@ -298,7 +255,9 @@ void handleAdminPage(HTTPRequest * req, HTTPResponse * res) { res->println("

You found the secret administrator page!

"); res->println("

Go back

"); res->println("
"); - } else { + } + else + { res->printStd(header); res->setStatusCode(403); res->setStatusText("Unauthorized"); @@ -309,7 +268,8 @@ void handleAdminPage(HTTPRequest * req, HTTPResponse * res) { } // Just a simple page for demonstration, very similar to the root page. -void handlePublicPage(HTTPRequest * req, HTTPResponse * res) { +void handlePublicPage(HTTPRequest * req, HTTPResponse * res) +{ res->setHeader("Content-Type", "text/html"); res->println(""); res->println(""); @@ -317,7 +277,7 @@ void handlePublicPage(HTTPRequest * req, HTTPResponse * res) { res->println(""); res->println("

Hello World!

"); res->print("

Your server is running for "); - res->print((int)(millis()/1000), DEC); + res->print((int)(millis() / 1000), DEC); res->println(" seconds.

"); res->println("

Go back

"); res->println(""); @@ -325,7 +285,8 @@ void handlePublicPage(HTTPRequest * req, HTTPResponse * res) { } // For details on the implementation of the hanlder functions, refer to the Static-Page example. -void handleRoot(HTTPRequest * req, HTTPResponse * res) { +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ res->setHeader("Content-Type", "text/html"); res->println(""); res->println(""); @@ -333,13 +294,14 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) { res->println(""); res->println("

Hello World!

"); res->println("

This is the authentication and authorization example. When asked for login " - "information, try admin/secret or user/test.

"); + "information, try admin/secret or user/test.

"); res->println("

Go to: Internal Page | Public Page

"); res->println(""); res->println(""); } -void handle404(HTTPRequest * req, HTTPResponse * res) { +void handle404(HTTPRequest * req, HTTPResponse * res) +{ req->discardRequestBody(); res->setStatusCode(404); res->setStatusText("Not Found"); @@ -350,3 +312,70 @@ void handle404(HTTPRequest * req, HTTPResponse * res) { res->println("

404 Not Found

The requested resource was not found on this server.

"); res->println(""); } + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Authentication on "); Serial.println(ARDUINO_BOARD); + + // Connect to WiFi + Serial.println("Setting up WiFi"); + WiFi.begin(WIFI_SSID, WIFI_PSK); + + while (WiFi.status() != WL_CONNECTED) + { + Serial.print("."); + delay(500); + } + + Serial.print("Connected. IP="); + Serial.println(WiFi.localIP()); + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * nodeInternal = new ResourceNode("/internal", "GET", &handleInternalPage); + ResourceNode * nodeAdmin = new ResourceNode("/internal/admin", "GET", &handleAdminPage); + ResourceNode * nodePublic = new ResourceNode("/public", "GET", &handlePublicPage); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the nodes to the server + secureServer.registerNode(nodeRoot); + secureServer.registerNode(nodeInternal); + secureServer.registerNode(nodeAdmin); + secureServer.registerNode(nodePublic); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + // Add the middleware. These functions will be called globally for every request + // Note: The functions are called in the order they are added to the server. + // This means, we need to add the authentication middleware first, because the + // authorization middleware needs the headers that will be set by the authentication + // middleware (First we check the identity, then we see what the user is allowed to do) + secureServer.addMiddleware(&middlewareAuthentication); + secureServer.addMiddleware(&middlewareAuthorization); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/Authentication/cert.h b/examples/Authentication/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/Authentication/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/Authentication/private_key.h b/examples/Authentication/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/Authentication/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/HTML-Forms/HTML-Forms.ino b/examples/HTML-Forms/HTML-Forms.ino index b29dfba..d4ca00f 100644 --- a/examples/HTML-Forms/HTML-Forms.ino +++ b/examples/HTML-Forms/HTML-Forms.ino @@ -1,24 +1,24 @@ /** - * Example for the ESP32 HTTP(S) Webserver - * - * IMPORTANT NOTE: - * To run this script, you need to - * 1) Enter your WiFi SSID and PSK below this comment - * 2) Make sure to have certificate data available. You will find a - * shell script and instructions to do so in the library folder - * under extras/ - * - * This script will install an HTTPS Server on your ESP32 with the following - * functionalities: - * - Show simple page on web server root that includes some HTML Forms - * - Define a POST handler that handles the forms using the HTTPBodyParser API - * provided by the library. - * - 404 for everything else - */ + Example for the ESP32 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, you need to + 1) Enter your WiFi SSID and PSK below this comment + 2) Make sure to have certificate data available. You will find a + shell script and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your ESP32 with the following + functionalities: + - Show simple page on web server root that includes some HTML Forms + - Define a POST handler that handles the forms using the HTTPBodyParser API + provided by the library. + - 404 for everything else +*/ // TODO: Configure your WiFi here -#define WIFI_SSID "" -#define WIFI_PSK "" +#define WIFI_SSID "your_ssid" +#define WIFI_PSK "12345678" // Include certificate data (see note above) #include "cert.h" @@ -42,11 +42,11 @@ // We need to specify some content-type mapping, so the resources get delivered with the // right content type and are displayed correctly in the browser -char contentTypes[][2][32] = { - {".txt", "text/plain"}, - {".html", "text/html"}, - {".png", "image/png"}, - {".jpg", "image/jpg"}, +char contentTypes[][2][32] = +{ + {".txt", "text/plain"}, + {".png", "image/png"}, + {".jpg", "image/jpg"}, {"", ""} }; @@ -55,33 +55,31 @@ using namespace httpsserver; // Create an SSL certificate object from the files included above SSLCert cert = SSLCert( - example_crt_DER, example_crt_DER_len, - example_key_DER, example_key_DER_len -); + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); // Create an SSL-enabled server that uses the certificate // The contstructor takes some more parameters, but we go for default values here. HTTPSServer secureServer = HTTPSServer(&cert); -// Declare some handler functions for the various URLs on the server -// See the static-page example for how handler functions work. -// The comments in setup() describe what each handler function does in this example. -void handleRoot(HTTPRequest * req, HTTPResponse * res); -void handleFormUpload(HTTPRequest * req, HTTPResponse * res); -void handleFormEdit(HTTPRequest * req, HTTPResponse * res); -void handleFile(HTTPRequest * req, HTTPResponse * res); -void handleDirectory(HTTPRequest * req, HTTPResponse * res); -void handle404(HTTPRequest * req, HTTPResponse * res); - -// As we have a file editor where the content of a file is pasted into a
"); res->println(""); res->println(""); } + res->println(""); - - } else { // method != GET + } + else + { + // method != GET // Assume POST request. Contains submitted data. res->println("File Edited

File Edited

"); - - // The form is submitted with the x-www-form-urlencoded content type, so we need the - // HTTPURLEncodedBodyParser to read the fields. - // Note that the content of the file's content comes from a
"); + res->println(""); + res->println(""); + } + + res->println(""); + } + else + { + // method != GET + // Assume POST request. Contains submitted data. + res->println("File Edited

File Edited

"); + HTTPURLEncodedBodyParser parser(req); + std::string filename; + bool savedFile = false; + + while (parser.nextField()) + { + std::string name = parser.getFieldName(); + + if (name == "filename") + { + char buf[512]; + size_t readLength = parser.read((byte *)buf, 512); + filename = std::string("/public/") + std::string(buf, readLength); + } + else if (name == "content") + { + if (filename == "") + { + res->println("

Error: form contained content before filename.

"); + break; + } + + size_t fieldLength = 0; + File file = SPIFFS.open(filename.c_str(), "w"); + savedFile = true; + + while (!parser.endOfField()) + { + byte buf[512]; + size_t readLength = parser.read(buf, 512); + file.write(buf, readLength); + fieldLength += readLength; + } + + file.close(); + res->printf("

Saved %d bytes to %s

", int(fieldLength), filename.c_str()); + } + else + { + res->printf("

Unexpected field %s

", name.c_str()); + } + } + + if (!savedFile) + { + res->println("

No file to save...

"); + } + + res->println(""); + } +} + +void handleDirectory(HTTPRequest * req, HTTPResponse * res) +{ + res->println("File Listing"); + File d = SPIFFS.open("/public"); + + if (!d.isDirectory()) + { + res->println("

No files found.

"); + } + else + { + res->println("

File Listing

"); + res->println("
    "); + + File f = d.openNextFile(); + + while (f) + { + std::string pathname(f.name()); + res->printf("
  • %s", pathname.c_str(), pathname.c_str()); + + if (pathname.rfind(".txt") != std::string::npos) + { + std::string filename = pathname.substr(8); // Remove /public/ + res->printf(" [edit]", filename.c_str()); + } + + res->println("
  • "); + f = d.openNextFile(); + } + + res->println("
"); + } + res->println(""); +} + +void handleFile(HTTPRequest * req, HTTPResponse * res) +{ + std::string filename = req->getRequestString(); + + // Check if the file exists + if (!SPIFFS.exists(filename.c_str())) + { + // Send "404 Not Found" as response, as the file doesn't seem to exist + res->setStatusCode(404); + res->setStatusText("Not found"); + res->println("404 Not Found"); + + return; + } + + File file = SPIFFS.open(filename.c_str()); + + // Set length + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + + // Content-Type is guessed using the definition of the contentTypes-table defined above + int cTypeIdx = 0; + + do + { + if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) + { + res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); + break; + } + + cTypeIdx += 1; + } while (strlen(contentTypes[cTypeIdx][0]) > 0); + + // Read the file and write it to the response + uint8_t buffer[256]; + size_t length = 0; + + do + { + length = file.read(buffer, 256); + res->write(buffer, length); + } while (length > 0); + + file.close(); +} + +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + // Discard request body, if we received any + // We do this, as this is the default node and may also server POST/PUT requests + req->discardRequestBody(); + + // Set the response status + res->setStatusCode(404); + res->setStatusText("Not Found"); + + // Set content type of the response + res->setHeader("Content-Type", "text/html"); + + // Write a tiny HTML page + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting HTML_Forms on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // Setup filesystem + if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed"); + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); + ResourceNode * nodeFormEdit = new ResourceNode("/edit", "GET", &handleFormEdit); + ResourceNode * nodeFormEditDone = new ResourceNode("/edit", "POST", &handleFormEdit); + ResourceNode * nodeDirectory = new ResourceNode("/public", "GET", &handleDirectory); + ResourceNode * nodeFile = new ResourceNode("/public/*", "GET", &handleFile); + + // 404 node has no URL as it is used for all requests that don't match anything else + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root nodes to the server + secureServer.registerNode(nodeRoot); + secureServer.registerNode(nodeFormUpload); + secureServer.registerNode(nodeFormEdit); + secureServer.registerNode(nodeFormEditDone); + secureServer.registerNode(nodeDirectory); + secureServer.registerNode(nodeFile); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/HTML-Forms/cert.h b/examples/WT32_ETH01/HTML-Forms/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/HTML-Forms/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/HTML-Forms/private_key.h b/examples/WT32_ETH01/HTML-Forms/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/HTML-Forms/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/HTTPS-and-HTTP/HTTPS-and-HTTP.ino b/examples/WT32_ETH01/HTTPS-and-HTTP/HTTPS-and-HTTP.ino new file mode 100644 index 0000000..97875e3 --- /dev/null +++ b/examples/WT32_ETH01/HTTPS-and-HTTP/HTTPS-and-HTTP.ino @@ -0,0 +1,176 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Make sure to have certificate data available. You will find a + shell script (create_cert.sh) and instructions to do so in the library folder + under extras/ + + This script will install an HTTP Server on port 80 and an HTTPS server + on port 443 of your WT32_ETH01 with the following functionalities: + - Show simple page on web server root + - 404 for everything else + + The comments in this script focus on making both protocols available, + for setting up the server itself, see Static-Page. +*/ + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// Includes for the server +// Note: We include HTTPServer and HTTPSServer +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +// Create an SSL certificate object from the files included above +SSLCert cert = SSLCert( + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); + +// First, we create the HTTPSServer with the certificate created above +HTTPSServer secureServer = HTTPSServer(&cert); + +// Additionally, we create an HTTPServer for unencrypted traffic +HTTPServer insecureServer = HTTPServer(); + +// The hanlder functions are the same as in the Static-Page example. +// The only difference is the check for isSecure in the root handler + +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ + res->setHeader("Content-Type", "text/html"); + + res->println(""); + res->println(""); + res->println("Hello World!"); + res->println(""); + res->println("

Hello World!

"); + + res->print("

Your server is running for "); + res->print((int)(millis() / 1000), DEC); + res->println(" seconds.

"); + + // You can check if you are connected over a secure connection, eg. if you + // want to use authentication and redirect the user to a secure connection + // for that + if (req->isSecure()) + { + res->println("

You are connected via HTTPS.

"); + } + else + { + res->println("

You are connected via HTTP.

"); + } + + res->println(""); + res->println(""); +} + +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + req->discardRequestBody(); + res->setStatusCode(404); + res->setStatusText("Not Found"); + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting HTTPS_and_HTTP on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root node to the servers. We can use the same ResourceNode on multiple + // servers (you could also run multiple HTTPS servers) + secureServer.registerNode(nodeRoot); + insecureServer.registerNode(nodeRoot); + + // We do the same for the default Node + secureServer.setDefaultNode(node404); + insecureServer.setDefaultNode(node404); + + Serial.println("Starting HTTPS server..."); + secureServer.start(); + Serial.println("Starting HTTP server..."); + insecureServer.start(); + + if (secureServer.isRunning() && insecureServer.isRunning()) + { + Serial.println("Servers ready."); + } +} + +void loop() +{ + // We need to call both loop functions here + secureServer.loop(); + insecureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/HTTPS-and-HTTP/cert.h b/examples/WT32_ETH01/HTTPS-and-HTTP/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/HTTPS-and-HTTP/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/HTTPS-and-HTTP/private_key.h b/examples/WT32_ETH01/HTTPS-and-HTTP/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/HTTPS-and-HTTP/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Middleware/Middleware.ino b/examples/WT32_ETH01/Middleware/Middleware.ino new file mode 100644 index 0000000..6605114 --- /dev/null +++ b/examples/WT32_ETH01/Middleware/Middleware.ino @@ -0,0 +1,188 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Make sure to have certificate data available. You will find a + shell script (create_cert.sh) and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your WT32_ETH01 with the following + functionalities: + - Show simple page on web server root + - Run a middleware that logs every request + - 404 for everything else +*/ + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// For the middleware +#include + +// Includes for the server +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +// Create an SSL certificate object from the files included above +SSLCert cert = SSLCert( + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); + +// Create an SSL-enabled server that uses the certificate +// The contstructor takes some more parameters, but we go for default values here. +HTTPSServer secureServer = HTTPSServer(&cert); + +// Declare a middleware function. +// Parameters: +// req: Request data, can be used to access URL, HTTP Method, Headers, ... +// res: Response data, can be used to access HTTP Status, Headers, ... +// next: This function is used to pass control down the chain. If you have done your work +// with the request object, you may decide if you want to process the request. +// If you do so, you call the next() function, and the next middleware function (if +// there is any) or the actual requestHandler will be called. +// If you want to skip the request, you do not call next, and set for example status +// code 403 on the response to show that the user is not allowed to access a specific +// resource. +// The Authentication examples provides more details on this. +// We want to log the following information for every request: +// - Response Status +// - Request Method +// - Request String (URL + Parameters) +void middlewareLogging(HTTPRequest * req, HTTPResponse * res, std::function next) +{ + // We want to print the response status, so we need to call next() first. + next(); + // After the call, the status is (hopefully) set by the handler function, so we can + // access it for logging. + Serial.printf("middlewareLogging(): %3d\t%s\t\t%s\n", + // Status code (like: 200) + res->getStatusCode(), + // Method used for the request (like: GET) + req->getMethod().c_str(), + // Request string (like /index.html) + req->getRequestString().c_str()); +} + +// For details on the implementation of the hanlder functions, refer to the Static-Page example. +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Hello World!"); + res->println(""); + res->println("

Hello World!

"); + res->print("

Your server is running for "); + res->print((int)(millis() / 1000), DEC); + res->println(" seconds.

"); + res->println(""); + res->println(""); +} + +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + req->discardRequestBody(); + res->setStatusCode(404); + res->setStatusText("Not Found"); + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Middleware on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + // Add the middleware. The function will be called globally for every request + // Note: The functions are called in the order they are added to the server. + // Also, if you want a middleware to handle only specific requests, you can check + // the URL within the middleware function. + secureServer.addMiddleware(&middlewareLogging); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/Middleware/cert.h b/examples/WT32_ETH01/Middleware/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/Middleware/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Middleware/private_key.h b/examples/WT32_ETH01/Middleware/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/Middleware/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Parameter-Validation/Parameter-Validation.ino b/examples/WT32_ETH01/Parameter-Validation/Parameter-Validation.ino new file mode 100644 index 0000000..7e2cf2a --- /dev/null +++ b/examples/WT32_ETH01/Parameter-Validation/Parameter-Validation.ino @@ -0,0 +1,312 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, you need to + 1) Make sure to have certificate data available. You will find a + shell script (create_cert.sh) and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your WT32_ETH01 with the following + functionalities: + - Shows you a page with some LEDs and allow you to turn them on or off + Parameters for the URLs are checked, so that you cannot address non-existing objects + - 404 for everything else + If you want to see the LEDs, connect them to GPIOs 33 (red), 25 (yellow), 26 (green) + and 27 (blue). +*/ + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// We use strings +#include + +// Includes for the server +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +// Create an SSL certificate object from the files included above +SSLCert cert = SSLCert( + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); + +// Create an SSL-enabled server that uses the certificate +// The contstructor takes some more parameters, but we go for default values here. +HTTPSServer secureServer = HTTPSServer(&cert); + +// A class that defines an LED +class LED +{ + public: + /** Name for the LED */ + const std::string _name; + /** Pin that it's connected to */ + const uint8_t _pin; + /** Current state */ + bool _on; + /** Constructor */ + LED(const std::string name, uint8_t pin): _name(name), _pin(pin) + { + _on = false; + pinMode(pin, OUTPUT); + } + + /** Method to turn the led on or of */ + void setOn(bool on) + { + digitalWrite(_pin, on ? HIGH : LOW); + _on = on; + } +}; + +// We create some LEDs: +#define LEDCOUNT 4 +LED myLEDs[LEDCOUNT] = +{ + LED("Red LED", 33), + LED("Yellow LED", 25), + LED("Green LED", 26), + LED("Blue LED", 27) +}; + +// Root node, will show the LEDs that are available +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ + // We will deliver an HTML page + res->setHeader("Content-Type", "text/html"); + + // Write the response page + res->println(""); + res->println("Parameter Validation Example"); + res->println(""); + + // Iterate over the LEDs. + for (int id = 0; id < LEDCOUNT; id++) + { + LED * led = &myLEDs[id]; + + res->print( + "
print(led->_on ? "on" : "off"); + res->print( + "\" " + "action=\"/led/" + ); + + res->print(id); + res->print("/"); + res->print(led->_on ? 0 : 1); + res->print( + "\">" + "

" + ); + res->printStd(led->_name); + res->print("

"); + } + + res->print( + "
" + "

To see that the validator functions are working as expected, you can e.g. call .

" + "
" + "" + "" + ); +} + +// Node to switch an LED on or off +// This is the handler for our post callback. We can work with the parameters without further +// validation, as the server assures this function only gets called if the validation succeeded. +void handleSwitch(HTTPRequest * req, HTTPResponse * res) +{ + // POST, so drain the input, if any + req->discardRequestBody(); + + // Get access to the parameters + ResourceParameters * params = req->getParams(); + + // Get the LED that is requested. + // Note that we can call stoi safely without further validation, as we + // defined that is has to be an unsigned integer and must not be >LEDCOUNT-1 + LED * led = &myLEDs[std::stoi(params->getPathParameter(0))]; + + // Set the state of the LED. The value of the parameter can only be "0" or "1" here, + // otherwise the server would not have called the handler. + led->setOn(params->getPathParameter(1) != "0"); + + // Redirect the user to the main page + res->setStatusCode(303); + // This should make the browser do a GET / + res->setStatusText("See Other"); + res->setHeader("Location", "/"); + res->println("Redirecting..."); +} + +// Validation function for the LED ID and state +// This function is the validator for the second parameter of the POST /led route. +// It accepts only the strings "0" (off) and "1" (on) +bool validateLEDState(std::string s) +{ + return s == "0" || s == "1"; +} + +// This function is a validator for the first parameter of the POST /led route. +// We did check before that the parameter is an integer, now we check its range. +bool validateLEDID(std::string s) +{ + uint32_t id = std::stoul(s); + + return id < LEDCOUNT; +} + +// Default handler for resources that do not exist +// For details to this function, see the Static-Page example +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + req->discardRequestBody(); + res->setStatusCode(404); + res->setStatusText("Not Found"); + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Parameter_Validation on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // We create a node for the main page of the server, available via get + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + + // This node will turn an LED on or of. It has two parameters: + // 1) The ID of the LED (0..LEDCOUNT) + // 2) The new state (0..1) + // For more information on path parameters in general, see the Parameters example. + ResourceNode * nodeSwitch = new ResourceNode("/led/*/*", "POST", &handleSwitch); + + // We want to use parameter validation. The ResourceNode class provides the method + // addPathParamValidator() for that. This method takes two parameters: + // 1) The index of the parameter that you want to validate, so for the first wildcard + // in the route pattern that has been specified above, it's 0, and for the second + // parameter it's 1. + // 2) A function pointer that takes an std::string as parameter and returns a bool. + // That bool should be true if the parameter is considered valid. + // All those functions are called in the order in that they have been added. So if + // you want check if a parameter is an integer and then do some calculation with it, + // make sure to add the integer-check first and the other function later. + // + // If any of the functions returns false, the URL is considered to be invalid completely. + // In this case, the server will return with a static 400 Bad Request response. + // + // For convenience, the ValidatorFunctions.hpp include file already provides some useful + // and common checks (integer, non-empty, ...). Have a look at it before you start + // implementing your own checks to save time! + + // First we will take care of the LED ID. This ID should be... + // ... an unsigned integer ... + nodeSwitch->addPathParamValidator(0, &validateUnsignedInteger); + // ... and within the range of known IDs. + // We can treat the parameter safely as integer in this validator, as all validators + // are executed in order and validateUnsignedInteger has been run before. + nodeSwitch->addPathParamValidator(0, &validateLEDID); + + // The second parameter should either be 0 or 1. We use our custom validateLEDState() validator for this: + nodeSwitch->addPathParamValidator(1, &validateLEDState); + + // Not found node + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + // And the switch node + secureServer.registerNode(nodeSwitch); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/Parameter-Validation/cert.h b/examples/WT32_ETH01/Parameter-Validation/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/Parameter-Validation/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Parameter-Validation/private_key.h b/examples/WT32_ETH01/Parameter-Validation/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/Parameter-Validation/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Parameters/Parameters.ino b/examples/WT32_ETH01/Parameters/Parameters.ino new file mode 100644 index 0000000..051b40f --- /dev/null +++ b/examples/WT32_ETH01/Parameters/Parameters.ino @@ -0,0 +1,409 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Make sure to have certificate data available. You will find a + shell script (create_cert.sh) and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your WT32_ETH01 with the following + functionalities: + - Show page on web server root that will display some SVG smileys. + Using a simple HTML form and GET parameters, you can change the colors + of those images. + - Provide the svg image with an optional GET parameter that allows to + modify the background color, URL: /images/awesome.svg + - Provide an example that shows how wildcard URL parameters get parsed + URL: /urlparam/some/thing + - 404 for everything else +*/ + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// Includes for the server +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +// Create an SSL certificate object from the files included above +SSLCert cert = SSLCert( + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); + +// Create an SSL-enabled server that uses the certificate +// The contstructor takes some more parameters, but we go for default values here. +HTTPSServer secureServer = HTTPSServer(&cert); + +// Handler functions + +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ + // We will deliver an HTML page + res->setHeader("Content-Type", "text/html"); + + // Write the response page + res->println(""); + res->println(""); + res->println("Hello World!"); + res->println(""); + res->println(""); + + res->println("

Query Parameters

"); + res->println("

The parameters after the question mark in your URL.

"); + + // Show a form to select a color to colorize the faces + // We pass the selection as get parameter "shades" to this very same page, + // so we can evaluate it below + res->println("
Show me faces in shades of "); + res->println(""); + res->println(""); + res->println("
"); + + // Get the params to check if the user did select something + ResourceParameters * params = req->getParams(); + std::string paramName = "shades"; + + // Print 6 faces + for (int i = 0; i < 6; i++) + { + // Include the image of the handleSVG function with a specific color code + res->print("getQueryParameter(paramName, paramVal)) + { + if (paramVal == "red" || paramVal == "magenta" || paramVal == "yellow" || paramVal == "rainbow") + { + r = 128 + random(0, 128); + } + + if (paramVal == "green" || paramVal == "cyan" || paramVal == "yellow" || paramVal == "rainbow") + { + g = 128 + random(0, 128); + } + + if (paramVal == "blue" || paramVal == "magenta" || paramVal == "cyan" || paramVal == "rainbow") + { + b = 128 + random(0, 128); + } + } + + // Print the random color. As the HTTPResponse extends the Print interface, we can make use of that. + res->print(r, HEX); + res->print(g, HEX); + res->print(b, HEX); + + res->print("\" alt=\"Awesome!\" />"); + } + + res->println("

You'll find another demo here.

"); + + // Link to the path parameter demo + res->println("

Path Parameters

"); + res->println("

The parameters derived from placeholders in your path, like /foo/bar.

"); + res->println("

You'll find the demo here.

"); + + res->println(""); + res->println(""); +} + +// This callback responds with an SVG image to a GET request. The icon is the "awesome face". +// (borrowed from https://commons.wikimedia.org/wiki/File:718smiley.svg) +// +// If the color query parameter is set (so the URL is like awesome.svg?color=fede58), the +// background of our awesome face is changed. +void handleSVG(HTTPRequest * req, HTTPResponse * res) +{ + // Get access to the parameters + ResourceParameters * params = req->getParams(); + + // Set SVG content type + res->setHeader("Content-Type", "image/svg+xml"); + + // Set a default color + std::string fillColor = "fede58"; + + // Get request parameter (like awesome.svg?color=ff0000) and validate it + std::string colorParamName = "color"; + + // Check that the parameter is set and retrieve it. + // The getQueryParameter function will modify the second parameter, but only if the query + // parameter is set. + std::string requestColor; + + if (params->getQueryParameter(colorParamName, requestColor)) + { + // Check for correct length + if (requestColor.length() == 6) + { + bool colorOk = true; + + // Check that we only have characters within [0-9a-fA-F] + for (int i = 1; i < 6 && colorOk; i++) + { + if (!( + (requestColor[i] >= '0' && requestColor[i] <= '9' ) || + (requestColor[i] >= 'a' && requestColor[i] <= 'f' ) || + (requestColor[i] >= 'A' && requestColor[i] <= 'F' ) + )) + { + colorOk = false; + } + } + + // If validation was successful, replace the default color + if (colorOk) + { + fillColor = requestColor; + } + } + } + + // Print the SVG to the response: + res->print(""); + res->print(""); + res->print(""); + res->print("printStd(fillColor); + res->print("\"/>"); + res->print(""); + res->print(""); + res->print(""); + res->print(""); + res->print(""); + res->print(""); + res->print(""); + res->print(""); +} + +// This is a more generic demo for the query parameters. It makes use of the iterator +// interface to access them, which is useful if you do not know the paramter names in +// adavance. +void handleQueryDemo(HTTPRequest * req, HTTPResponse * res) +{ + // A word of warning: In this example, we use the query parameters and directly print + // them into the HTML output. We do this to simplify the demo. NEVER do this in a + // real application, as it allows cross-site-scripting. + res->setHeader("Content-Type", "text/html"); + + res->println(""); + res->println(""); + res->println(""); + res->println("Query Parameter Demo"); + res->println(""); + res->println(""); + res->println("

The following query paramters have been set:

"); + + // Start a table to display the parameters + res->println(""); + res->println(""); + // Iterate over the parameters. For more information, read about the C++ standard template library, + // especially about vectors and iterators. + ResourceParameters *params = req->getParams(); + + for (auto it = params->beginQueryParameters(); it != params->endQueryParameters(); ++it) + { + res->print(""); + } + + res->println("
KeyValue
"); + + // The iterator yields std::pairs of std::strings. The first value contains the parameter key + res->printStd((*it).first); + res->print(""); + + // and the second value contains the parameter value + res->printStd((*it).second); + res->println("
"); + + // You can retrieve the total parameter count from the parameters instance: + res->print("

There are a total of "); + res->print(params->getQueryParameterCount()); + res->print(" parameters, with "); + res->print(params->getQueryParameterCount(true)); + res->println(" unique keys.

"); + + res->println("

Go back to main page.

"); + res->println(""); + res->println(""); +} + +// This is a simple handler function that will show the content of URL parameters. +// If you call for example /urlparam/foo/bar, you will get the parameter values +// "foo" and "bar" provided by the ResourceParameters. +void handlePathParam(HTTPRequest * req, HTTPResponse * res) +{ + // Get access to the parameters + ResourceParameters * params = req->getParams(); + + // Set a simple content type + res->setHeader("Content-Type", "text/plain"); + + // The url pattern is: /urlparam/*/* + // This will make the content for the first parameter available on index 0, + // and the second wildcard as index 1. + // getPathParameter will - like getQueryParameter - write the value to the second parameter, + // and return true, if the index is valid. Otherwise it returns false and leaves the second + // parameter as it is. + + std::string parameter1, parameter2; + + // Print the first parameter value + if (params->getPathParameter(0, parameter1)) + { + res->print("Parameter 1: "); + res->printStd(parameter1); + } + + res->println(); + + // Print the second parameter value + if (params->getPathParameter(1, parameter2)) + { + res->print("Parameter 2: "); + res->printStd(parameter2); + } + + res->println("\n\nChange the parameters in the URL to see how they get parsed!"); +} + +// For details to this function, see the Static-Page example +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + req->discardRequestBody(); + res->setStatusCode(404); + res->setStatusText("Not Found"); + res->setHeader("Content-Type", "text/html"); + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Parameters on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * nodeSVG = new ResourceNode("/images/awesome.svg", "GET", &handleSVG); + ResourceNode * nodeQueryDemo = new ResourceNode("/queryparams", "GET", &handleQueryDemo); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Path parameters + // If you want (for example) to return a specific instance of an object type by its ID + // you can use URLs like /led/1, led/2, ... - And you do not need to register one Resource + // Node per ID, but you can use wildcards in the route definition. The following route + // has two wildcards, and will match for example to /urlparam/foo/bar, where "foo" and "bar" + // are accessible parameters in the handler function. + // Note: The wildcards can only be used between slashes at the moment (so /urlparam* would + // not work). + ResourceNode * nodeURLParam = new ResourceNode("/urlparam/*/*", "GET", &handlePathParam); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + + // Add the SVG image + secureServer.registerNode(nodeSVG); + + // Query parameter demo + secureServer.registerNode(nodeQueryDemo); + + // Add the path parameter + // Note: The order of nodes may become important here. If you have one node for "/led" (e.g. list of LEDs) + // and one node for /led/* (LED details), you should register the non-parameterized version first. The server + // follows a first-match policy. If you would register the details node first, a call to /led/ will be targetted + // at the details handler function with an empty parameter, which is probably not what you want. + secureServer.registerNode(nodeURLParam); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/Parameters/cert.h b/examples/WT32_ETH01/Parameters/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/Parameters/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Parameters/private_key.h b/examples/WT32_ETH01/Parameters/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/Parameters/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Put-Post-Echo/Put-Post-Echo.ino b/examples/WT32_ETH01/Put-Post-Echo/Put-Post-Echo.ino new file mode 100644 index 0000000..9a6eff1 --- /dev/null +++ b/examples/WT32_ETH01/Put-Post-Echo/Put-Post-Echo.ino @@ -0,0 +1,212 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, your need to + 1) Make sure to have certificate data available. You will find a + shell script (create_cert.sh) and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your WT32_ETH01 with the following + functionalities: + - Show simple page on web server root + - Provide an Echo-Service for PUT/POST requests on / + - 404 for everything else +*/ + +// Include certificate data (see note above) +#include "cert.h" +#include "private_key.h" + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// Includes for the server +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +// Create an SSL certificate object from the files included above +SSLCert cert = SSLCert( + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); + +// Create an SSL-enabled server that uses the certificate +// The contstructor takes some more parameters, but we go for default values here. +HTTPSServer secureServer = HTTPSServer(&cert); + +// Handler functions for the various URLs on the server +// The signature is always the same for those functions. They get two parameters, +// which are pointers to the request data (read request body, headers, ...) and +// to the response data (write response, set status code, ...) + +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/html"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->println(""); + res->println(""); + res->println("Hello World!"); + res->println(""); + res->println("

Hello World!

"); + res->print("

Your server is running for "); + // A bit of dynamic data: Show the uptime + res->print((int)(millis() / 1000), DEC); + res->println(" seconds.

"); + res->print("

The easiest way to test the echo function is to use an HTTP debugging " + " tool where you are able to set a body and then let it call PUT or POST on" + " https://"); + res->print(WiFi.localIP()); + res->print("/. Otherwise you can use command line tools like curl:

" + "
cat <<EOF | curl --insecure -X PUT -d @- https://");
+  res->print(WiFi.localIP());
+  res->print("/\nHere goes your request \nbody\n"
+             "EOF
"); + res->println(""); + res->println(""); +} + +void handleEcho(HTTPRequest * req, HTTPResponse * res) +{ + // The echo callback will return the request body as response body. + + // We use text/plain for the response + res->setHeader("Content-Type", "text/plain"); + + // Stream the incoming request body to the response body + // Theoretically, this should work for every request size. + byte buffer[256]; + + // HTTPRequest::requestComplete can be used to check whether the + // body has been parsed completely. + while (!(req->requestComplete())) + { + // HTTPRequest::readBytes provides access to the request body. + // It requires a buffer, the max buffer length and it will return + // the amount of bytes that have been written to the buffer. + size_t s = req->readBytes(buffer, 256); + + // The response does not only implement the Print interface to + // write character data to the response but also the write function + // to write binary data to the response. + res->write(buffer, s); + } +} + +void handle404(HTTPRequest * req, HTTPResponse * res) +{ + // Discard request body, if we received any + // We do this, as this is the default node and may also server POST/PUT requests + req->discardRequestBody(); + + // Set the response status + res->setStatusCode(404); + res->setStatusText("Not Found"); + + // Set content type of the response + res->setHeader("Content-Type", "text/html"); + + // Write a tiny HTTP page + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Put_Post_Echo on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Register the echo handler. You can use the same handler function for multiple + // nodes. Note also that we now have three resource nodes for the / URL, so + // the server uses only the method to distinguish between them. + ResourceNode * nodeEchoPut = new ResourceNode("/", "PUT", &handleEcho); + ResourceNode * nodeEchoPost = new ResourceNode("/", "POST", &handleEcho); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + + // Add the nodes for the echo service + secureServer.registerNode(nodeEchoPut); + secureServer.registerNode(nodeEchoPost); + + // Add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/Put-Post-Echo/cert.h b/examples/WT32_ETH01/Put-Post-Echo/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/Put-Post-Echo/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Put-Post-Echo/private_key.h b/examples/WT32_ETH01/Put-Post-Echo/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/Put-Post-Echo/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/REST-API/REST-API.ino b/examples/WT32_ETH01/REST-API/REST-API.ino new file mode 100644 index 0000000..2281cad --- /dev/null +++ b/examples/WT32_ETH01/REST-API/REST-API.ino @@ -0,0 +1,646 @@ +/** + Example for the WT32_ETH01 HTTP(S) Webserver + + IMPORTANT NOTE: + This example is a bit more complex than the other ones, so be careful to + follow all steps. + + Make sure to check out the more basic examples like Static-Page to understand + the fundamental principles of the API before proceeding with this sketch. + + To run this script, you need to + 1) Install the SPIFFS File uploader into your Arduino IDE to be able to + upload static data to the webserver. + Follow the instructions at: + https://github.com/me-no-dev/arduino-esp32fs-plugin + 2) Upload the static files from the data/ directory of the example to your + module's SPIFFs by using "ESP32 Sketch Data Upload" from the tools menu. + If you face any problems, read the description of the libraray mentioned + above. + Note: If mounting SPIFFS fails, the script will wait for a serial connection + (open your serial monitor!) and ask if it should format the SPIFFS partition. + You may need this before uploading the data + Note: Make sure to select a partition layout that allows for SPIFFS in the + boards menu + 4) Have the ArduinoJSON library installed and available. (Tested with Version 5.13.4) + You'll find it at: + https://arduinojson.org/ + + This script will install an HTTPS Server on your WT32_ETH01 with the following + functionalities: + - Serve static files from the SPIFFS's data/public directory + - Provide a REST API at /api to receive the asynchronous http requests + - /api/uptime provides access to the current system uptime + - /api/events allows to register or delete events to turn PINs on/off + at certain times. + - Use Arduino JSON for body parsing and generation of responses. + - The certificate is generated on first run and stored to the SPIFFS in + the cert directory (so that the client cannot retrieve the private key) +*/ + +////////////////////////////////////////////////// + +// For WT32_ETH01 +#define DEBUG_ETHERNET_WEBSERVER_PORT Serial + +// Debug Level from 0 to 4 +#define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 + +#include + +// Select the IP address according to your local network +IPAddress myIP(192, 168, 2, 232); +IPAddress myGW(192, 168, 2, 1); +IPAddress mySN(255, 255, 255, 0); + +// Google DNS Server IP +IPAddress myDNS(8, 8, 8, 8); + +////////////////////////////////////////////////// + +// We will use SPIFFS and FS +#include +#include + +// We use JSON as data format. Make sure to have the lib available +#include + +// Working with c++ strings +#include + +// Define the name of the directory for public files in the SPIFFS parition +#define DIR_PUBLIC "/public" + +// We need to specify some content-type mapping, so the resources get delivered with the +// right content type and are displayed correctly in the browser +char contentTypes[][2][32] = +{ + {".html", "text/html"}, + {".css", "text/css"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".png", "image/png"}, + {".jpg", "image/jpg"}, + {"", ""} +}; + +// Includes for the server +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +SSLCert * getCertificate(); + +// We use the following struct to store GPIO events: +#define MAX_EVENTS 20 +struct +{ + // is this event used (events that have been run will be set to false) + bool active; + // when should it be run? + unsigned long time; + // which GPIO should be changed? + int gpio; + // and to which state? + int state; +} events[MAX_EVENTS]; + +// We just create a reference to the server here. We cannot call the constructor unless +// we have initialized the SPIFFS and read or created the certificate +HTTPSServer * secureServer; + + +/** + This function will either read the certificate and private key from SPIFFS or + create a self-signed certificate and write it to SPIFFS for next boot +*/ +SSLCert * getCertificate() +{ + // Try to open key and cert file to see if they exist + File keyFile = SPIFFS.open("/key.der"); + File certFile = SPIFFS.open("/cert.der"); + + // If now, create them + if (!keyFile || !certFile || keyFile.size() == 0 || certFile.size() == 0) + { + Serial.println("No certificate found in SPIFFS, generating a new one for you."); + Serial.println("If you face a Guru Meditation, give the script another try (or two...)."); + Serial.println("This may take up to a minute, so please stand by :)"); + + SSLCert * newCert = new SSLCert(); + + // The part after the CN= is the domain that this certificate will match, in this + // case, it's esp32.local. + // However, as the certificate is self-signed, your browser won't trust the server + // anyway. + int res = createSelfSignedCert(*newCert, KEYSIZE_1024, "CN=esp32.local,O=acme,C=DE"); + + if (res == 0) + { + // We now have a certificate. We store it on the SPIFFS to restore it on next boot. + + bool failure = false; + + // Private key + keyFile = SPIFFS.open("/key.der", FILE_WRITE); + + if (!keyFile || !keyFile.write(newCert->getPKData(), newCert->getPKLength())) + { + Serial.println("Could not write /key.der"); + failure = true; + } + + if (keyFile) + keyFile.close(); + + // Certificate + certFile = SPIFFS.open("/cert.der", FILE_WRITE); + + if (!certFile || !certFile.write(newCert->getCertData(), newCert->getCertLength())) + { + Serial.println("Could not write /cert.der"); + failure = true; + } + + if (certFile) + certFile.close(); + + if (failure) + { + Serial.println("Certificate could not be stored permanently, generating new certificate on reboot..."); + } + + return newCert; + + } + else + { + // Certificate generation failed. Inform the user. + Serial.println("An error occured during certificate generation."); + Serial.print("Error code is 0x"); + Serial.println(res, HEX); + Serial.println("You may have a look at SSLCert.h to find the reason for this error."); + + return NULL; + } + + } + else + { + Serial.println("Reading certificate from SPIFFS."); + + // The files exist, so we can create a certificate based on them + size_t keySize = keyFile.size(); + size_t certSize = certFile.size(); + + uint8_t * keyBuffer = new uint8_t[keySize]; + + if (keyBuffer == NULL) + { + Serial.println("Not enough memory to load privat key"); + return NULL; + } + + uint8_t * certBuffer = new uint8_t[certSize]; + + if (certBuffer == NULL) + { + delete[] keyBuffer; + Serial.println("Not enough memory to load certificate"); + + return NULL; + } + + keyFile.read(keyBuffer, keySize); + certFile.read(certBuffer, certSize); + + // Close the files + keyFile.close(); + certFile.close(); + Serial.printf("Read %u bytes of certificate and %u bytes of key from SPIFFS\n", certSize, keySize); + return new SSLCert(certBuffer, certSize, keyBuffer, keySize); + } +} + +/** + This handler function will try to load the requested resource from SPIFFS's /public folder. + + If the method is not GET, it will throw 405, if the file is not found, it will throw 404. +*/ +void handleSPIFFS(HTTPRequest * req, HTTPResponse * res) +{ + + // We only handle GET here + if (req->getMethod() == "GET") + { + // Redirect / to /index.html + std::string reqFile = req->getRequestString() == "/" ? "/index.html" : req->getRequestString(); + + // Try to open the file + std::string filename = std::string(DIR_PUBLIC) + reqFile; + + // Check if the file exists + if (!SPIFFS.exists(filename.c_str())) + { + // Send "404 Not Found" as response, as the file doesn't seem to exist + res->setStatusCode(404); + res->setStatusText("Not found"); + res->println("404 Not Found"); + + return; + } + + File file = SPIFFS.open(filename.c_str()); + + // Set length + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + + // Content-Type is guessed using the definition of the contentTypes-table defined above + int cTypeIdx = 0; + + do + { + if (reqFile.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) + { + res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); + break; + } + + cTypeIdx += 1; + } while (strlen(contentTypes[cTypeIdx][0]) > 0); + + // Read the file and write it to the response + uint8_t buffer[256]; + size_t length = 0; + + do + { + length = file.read(buffer, 256); + res->write(buffer, length); + } while (length > 0); + + file.close(); + } + else + { + // If there's any body, discard it + req->discardRequestBody(); + // Send "405 Method not allowed" as response + res->setStatusCode(405); + res->setStatusText("Method not allowed"); + res->println("405 Method not allowed"); + } +} + +/** + This function will return the uptime in seconds as JSON object: + {"uptime": 42} +*/ +void handleGetUptime(HTTPRequest * req, HTTPResponse * res) +{ + // Create a buffer of size 1 (pretty simple, we have just one key here) + StaticJsonBuffer jsonBuffer; + // Create an object at the root + JsonObject& obj = jsonBuffer.createObject(); + + // Set the uptime key to the uptime in seconds + obj["uptime"] = millis() / 1000; + // Set the content type of the response + res->setHeader("Content-Type", "application/json"); + // As HTTPResponse implements the Print interface, this works fine. Just remember + // to use *, as we only have a pointer to the HTTPResponse here: + obj.printTo(*res); +} + +/** + This handler will return a JSON array of currently active events for GET /api/events +*/ +void handleGetEvents(HTTPRequest * req, HTTPResponse * res) +{ + // We need to calculate the capacity of the json buffer + int activeEvents = 0; + + for (int i = 0; i < MAX_EVENTS; i++) + { + if (events[i].active) + activeEvents++; + } + + // For each active event, we need 1 array element with 4 objects + const size_t capacity = JSON_ARRAY_SIZE(activeEvents) + activeEvents * JSON_OBJECT_SIZE(4); + + // DynamicJsonBuffer is created on the heap instead of the stack + DynamicJsonBuffer jsonBuffer(capacity); + JsonArray& arr = jsonBuffer.createArray(); + + for (int i = 0; i < MAX_EVENTS; i++) + { + if (events[i].active) + { + JsonObject& eventObj = arr.createNestedObject(); + eventObj["gpio"] = events[i].gpio; + eventObj["state"] = events[i].state; + eventObj["time"] = events[i].time; + // Add the index to allow delete and post to identify the element + eventObj["id"] = i; + } + } + + // Print to response + res->setHeader("Content-Type", "application/json"); + arr.printTo(*res); +} + +void handlePostEvent(HTTPRequest * req, HTTPResponse * res) +{ + // We expect an object with 4 elements and add some buffer + const size_t capacity = JSON_OBJECT_SIZE(4) + 180; + DynamicJsonBuffer jsonBuffer(capacity); + + // Create buffer to read request + char * buffer = new char[capacity + 1]; + memset(buffer, 0, capacity + 1); + + // Try to read request into buffer + size_t idx = 0; + + // while "not everything read" or "buffer is full" + while (!req->requestComplete() && idx < capacity) + { + idx += req->readChars(buffer + idx, capacity - idx); + } + + // If the request is still not read completely, we cannot process it. + if (!req->requestComplete()) + { + res->setStatusCode(413); + res->setStatusText("Request entity too large"); + res->println("413 Request entity too large"); + + // Clean up + delete[] buffer; + + return; + } + + // Parse the object + JsonObject& reqObj = jsonBuffer.parseObject(buffer); + + // Check input data types + bool dataValid = true; + + if (!reqObj.is("time") || !reqObj.is("gpio") || !reqObj.is("state")) + { + dataValid = false; + } + + // Check actual values + unsigned long eTime = 0; + int eGpio = 0; + int eState = LOW; + + if (dataValid) + { + eTime = reqObj["time"]; + + if (eTime < millis() / 1000) + dataValid = false; + + eGpio = reqObj["gpio"]; + + if (!(eGpio == 25 || eGpio == 26 || eGpio == 27 || eGpio == 32 || eGpio == 33)) + dataValid = false; + + eState = reqObj["state"]; + + if (eState != HIGH && eState != LOW) + dataValid = false; + } + + // Clean up, we don't need the buffer any longer + delete[] buffer; + + // If something failed: 400 + if (!dataValid) + { + res->setStatusCode(400); + res->setStatusText("Bad Request"); + res->println("400 Bad Request"); + + return; + } + + // Try to find an inactive event in the list to write the data to + int eventID = -1; + + for (int i = 0; i < MAX_EVENTS && eventID == -1; i++) + { + if (!events[i].active) + { + eventID = i; + events[i].gpio = eGpio; + events[i].time = eTime; + events[i].state = eState; + events[i].active = true; + } + } + + // Check if we could store the event + if (eventID > -1) + { + // Create a buffer for the response + StaticJsonBuffer resBuffer; + + // Create an object at the root + JsonObject& resObj = resBuffer.createObject(); + + // Set the uptime key to the uptime in seconds + resObj["gpio"] = events[eventID].gpio; + resObj["state"] = events[eventID].state; + resObj["time"] = events[eventID].time; + resObj["id"] = eventID; + + // Write the response + res->setHeader("Content-Type", "application/json"); + resObj.printTo(*res); + + } + else + { + // We could not store the event, no free slot. + res->setStatusCode(507); + res->setStatusText("Insufficient storage"); + res->println("507 Insufficient storage"); + } +} + +/** + This handler will delete an event (meaning: deactive the event) +*/ +void handleDeleteEvent(HTTPRequest * req, HTTPResponse * res) +{ + // Access the parameter from the URL. See Parameters example for more details on this + ResourceParameters * params = req->getParams(); + size_t eid = std::atoi(params->getPathParameter(0).c_str()); + + if (eid < MAX_EVENTS) + { + // Set the inactive flag + events[eid].active = false; + // And return a successful response without body + res->setStatusCode(204); + res->setStatusText("No Content"); + } + else + { + // Send error message + res->setStatusCode(400); + res->setStatusText("Bad Request"); + res->println("400 Bad Request"); + } +} + +void setup() +{ + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting REST_API on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // Set the pins that we will use as output pins + pinMode(25, OUTPUT); + pinMode(26, OUTPUT); + pinMode(27, OUTPUT); + pinMode(32, OUTPUT); + pinMode(33, OUTPUT); + + // Try to mount SPIFFS without formatting on failure + if (!SPIFFS.begin(false)) + { + // If SPIFFS does not work, we wait for serial connection... + while (!Serial); + delay(1000); + + // Ask to format SPIFFS using serial interface + Serial.print("Mounting SPIFFS failed. Try formatting? (y/n): "); + while (!Serial.available()); + Serial.println(); + + // If the user did not accept to try formatting SPIFFS or formatting failed: + if (Serial.read() != 'y' || !SPIFFS.begin(true)) + { + Serial.println("SPIFFS not available. Stop."); + while (true); + } + + Serial.println("SPIFFS has been formated."); + } + + Serial.println("SPIFFS has been mounted."); + + // Now that SPIFFS is ready, we can create or load the certificate + SSLCert *cert = getCertificate(); + + if (cert == NULL) + { + Serial.println("Could not load certificate. Stop."); + while (true); + } + + // Initialize event structure: + for (int i = 0; i < MAX_EVENTS; i++) + { + events[i].active = false; + events[i].gpio = 0; + events[i].state = LOW; + events[i].time = 0; + } + + /////////////////////////////////////////////// + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // Create the server with the certificate we loaded before + secureServer = new HTTPSServer(cert); + + // We register the SPIFFS handler as the default node, so every request that does + // not hit any other node will be redirected to the file system. + ResourceNode * spiffsNode = new ResourceNode("", "", &handleSPIFFS); + secureServer->setDefaultNode(spiffsNode); + + // Add a handler that serves the current system uptime at GET /api/uptime + ResourceNode * uptimeNode = new ResourceNode("/api/uptime", "GET", &handleGetUptime); + secureServer->registerNode(uptimeNode); + + // Add the handler nodes that deal with modifying the events: + ResourceNode * getEventsNode = new ResourceNode("/api/events", "GET", &handleGetEvents); + secureServer->registerNode(getEventsNode); + ResourceNode * postEventNode = new ResourceNode("/api/events", "POST", &handlePostEvent); + secureServer->registerNode(postEventNode); + ResourceNode * deleteEventNode = new ResourceNode("/api/events/*", "DELETE", &handleDeleteEvent); + secureServer->registerNode(deleteEventNode); + + Serial.println("Starting server..."); + secureServer->start(); + + if (secureServer->isRunning()) + { + Serial.println("Server ready."); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer->loop(); + + // Here we handle the events + unsigned long now = millis() / 1000; + + for (int i = 0; i < MAX_EVENTS; i++) + { + // Only handle active events: + if (events[i].active) + { + // Only if the counter has recently been exceeded + if (events[i].time < now) + { + // Apply the state change + digitalWrite(events[i].gpio, events[i].state); + + // Deactivate the event so it doesn't fire again + events[i].active = false; + } + } + } + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/REST-API/cert.h b/examples/WT32_ETH01/REST-API/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/REST-API/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/REST-API/data/public/index.html b/examples/WT32_ETH01/REST-API/data/public/index.html new file mode 100644 index 0000000..be0a8bb --- /dev/null +++ b/examples/WT32_ETH01/REST-API/data/public/index.html @@ -0,0 +1,230 @@ + + + + esp32_https_server - REST API example + + + + + +

esp32_https_server - REST API example

+

Current system time: loading...

+ +

Current Events

+ + + + + + + + + + + +
TimeGPIONew State
+

Add a New Event

+
+ + + + + + + + + + + + + + + + + +
TimeGPIOState
sec + + + HIGH +
+
+

About This Example

+

This example shows you a web-page that can be used to control GPIOs 25, 26, 27, 32 and 33 to be turned on or off at a given time.

+

For simplicity, we use the system uptime here, which you can see at the top of the page.

+

The page uses the REST API provided at /api to commuicate with the ESP32.

+

GET /api/uptime returns the current system uptime:

+
{"uptime":42}
+

This node is polled every 10 seconds to stay synchronized.

+

GET /api/events returns the current list of events:

+
[{"gpio":27,"state":1,"time":42,"id":0}, ...]
+

This node is polled every 60 seconds to stay synchronized

+

POST /api/events can be used to add new events. The response object will contain an additional id that can be used to refer to the event.

+

DELETE /api/events/id will delete the event with the specified id. If events are executed (time < uptime), they are deleted as well.

+

The client side javascript synchronizes your view even if not explicit request is sent to the ESP32.

+ + \ No newline at end of file diff --git a/examples/WT32_ETH01/REST-API/data/public/jquery-3.3.1.min.js b/examples/WT32_ETH01/REST-API/data/public/jquery-3.3.1.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/examples/WT32_ETH01/REST-API/data/public/jquery-3.3.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("\n" + "\n" + "\n" + ); +} + +void setup() +{ + // Initialize the slots + for (int i = 0; i < MAX_CLIENTS; i++) + activeClients[i] = nullptr; + + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting WebSocket_Chat on "); Serial.print(ARDUINO_BOARD); + Serial.println(" with " + String(SHIELD_TYPE)); + Serial.println(WEBSERVER_WT32_ETH01_VERSION); + + // To be called before ETH.begin() + WT32_ETH01_onEvent(); + + //bool begin(uint8_t phy_addr=ETH_PHY_ADDR, int power=ETH_PHY_POWER, int mdc=ETH_PHY_MDC, int mdio=ETH_PHY_MDIO, + // eth_phy_type_t type=ETH_PHY_TYPE, eth_clock_mode_t clk_mode=ETH_CLK_MODE); + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); + ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); + + // Static IP, leave without this line to get IP via DHCP + //bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = 0, IPAddress dns2 = 0); + ETH.config(myIP, myGW, mySN, myDNS); + + WT32_ETH01_waitForConnect(); + + Serial.print(F("HTTP EthernetWebServer is @ IP : ")); + Serial.println(ETH.localIP()); + + /////////////////////////////////////////////// + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + + // The websocket handler can be linked to the server by using a WebsocketNode: + // (Note that the standard defines GET as the only allowed method here, + // so you do not need to pass it explicitly) + WebsocketNode * chatNode = new WebsocketNode("/chat", &ChatHandler::create); + + // Adding the node to the server works in the same way as for all other nodes + secureServer.registerNode(chatNode); + + // Finally, add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.print("Server ready. Open the following URL in multiple browser windows to start chatting: https://"); + Serial.println(WiFi.localIP()); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/WT32_ETH01/Websocket-Chat/cert.h b/examples/WT32_ETH01/Websocket-Chat/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/WT32_ETH01/Websocket-Chat/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/WT32_ETH01/Websocket-Chat/private_key.h b/examples/WT32_ETH01/Websocket-Chat/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/WT32_ETH01/Websocket-Chat/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/Websocket-Chat/Websocket-Chat.ino b/examples/Websocket-Chat/Websocket-Chat.ino index 989c3b6..fd4b621 100644 --- a/examples/Websocket-Chat/Websocket-Chat.ino +++ b/examples/Websocket-Chat/Websocket-Chat.ino @@ -1,24 +1,24 @@ /** - * Example for the ESP32 HTTP(S) Webserver - * - * IMPORTANT NOTE: - * To run this script, you need to - * 1) Enter your WiFi SSID and PSK below this comment - * 2) Make sure to have certificate data available. You will find a - * shell script and instructions to do so in the library folder - * under extras/ - * - * This script will install an HTTPS Server on your ESP32 with the following - * functionalities: - * - Show a chat interface on the root node / - * - Use a websocket to allow multiple clients to pass messages to each other - */ + Example for the ESP32 HTTP(S) Webserver + + IMPORTANT NOTE: + To run this script, you need to + 1) Enter your WiFi SSID and PSK below this comment + 2) Make sure to have certificate data available. You will find a + shell script and instructions to do so in the library folder + under extras/ + + This script will install an HTTPS Server on your ESP32 with the following + functionalities: + - Show a chat interface on the root node / + - Use a websocket to allow multiple clients to pass messages to each other +*/ #include // TODO: Configure your WiFi here -#define WIFI_SSID "" -#define WIFI_PSK "" +#define WIFI_SSID "your_ssid" +#define WIFI_PSK "12345678" // Max clients to be connected to the chat #define MAX_CLIENTS 4 @@ -42,93 +42,40 @@ using namespace httpsserver; // Create an SSL certificate object from the files included above ... SSLCert cert = SSLCert( - example_crt_DER, example_crt_DER_len, - example_key_DER, example_key_DER_len -); + example_crt_DER, example_crt_DER_len, + example_key_DER, example_key_DER_len + ); // ... and create a server based on this certificate. // The constructor has some optional parameters like the TCP port that should be used // and the max client count. For simplicity, we use a fixed amount of clients that is bound // to the max client count. HTTPSServer secureServer = HTTPSServer(&cert, 443, MAX_CLIENTS); -// Declare some handler functions for the various URLs on the server -// The signature is always the same for those functions. They get two parameters, -// which are pointers to the request data (read request body, headers, ...) and -// to the response data (write response, set status code, ...) -void handleRoot(HTTPRequest * req, HTTPResponse * res); -void handle404(HTTPRequest * req, HTTPResponse * res); - // As websockets are more complex, they need a custom class that is derived from WebsocketHandler -class ChatHandler : public WebsocketHandler { -public: - // This method is called by the webserver to instantiate a new handler for each - // client that connects to the websocket endpoint - static WebsocketHandler* create(); - - // This method is called when a message arrives - void onMessage(WebsocketInputStreambuf * input); - - // Handler function on connection close - void onClose(); +class ChatHandler : public WebsocketHandler +{ + public: + // This method is called by the webserver to instantiate a new handler for each + // client that connects to the websocket endpoint + static WebsocketHandler* create(); + + // This method is called when a message arrives + void onMessage(WebsocketInputStreambuf * input); + + // Handler function on connection close + void onClose(); }; // Simple array to store the active clients: ChatHandler* activeClients[MAX_CLIENTS]; -void setup() { - // Initialize the slots - for(int i = 0; i < MAX_CLIENTS; i++) activeClients[i] = nullptr; - - // For logging - Serial.begin(115200); - - // Connect to WiFi - Serial.println("Setting up WiFi"); - WiFi.begin(WIFI_SSID, WIFI_PSK); - while (WiFi.status() != WL_CONNECTED) { - Serial.print("."); - delay(500); - } - Serial.print("Connected. IP="); - Serial.println(WiFi.localIP()); - - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function - ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); - ResourceNode * node404 = new ResourceNode("", "GET", &handle404); - - // Add the root node to the server - secureServer.registerNode(nodeRoot); - - // The websocket handler can be linked to the server by using a WebsocketNode: - // (Note that the standard defines GET as the only allowed method here, - // so you do not need to pass it explicitly) - WebsocketNode * chatNode = new WebsocketNode("/chat", &ChatHandler::create); - - // Adding the node to the server works in the same way as for all other nodes - secureServer.registerNode(chatNode); - - // Finally, add the 404 not found node to the server. - // The path is ignored for the default node. - secureServer.setDefaultNode(node404); - - Serial.println("Starting server..."); - secureServer.start(); - if (secureServer.isRunning()) { - Serial.print("Server ready. Open the following URL in multiple browser windows to start chatting: https://"); - Serial.println(WiFi.localIP()); - } -} - -void loop() { - // This call will let the server do its work - secureServer.loop(); - - // Other code would go here... - delay(1); -} +// Handler functions for the various URLs on the server +// The signature is always the same for those functions. They get two parameters, +// which are pointers to the request data (read request body, headers, ...) and +// to the response data (write response, set status code, ...) -void handle404(HTTPRequest * req, HTTPResponse * res) { +void handle404(HTTPRequest * req, HTTPResponse * res) +{ // Discard request body, if we received any // We do this, as this is the default node and may also server POST/PUT requests req->discardRequestBody(); @@ -150,30 +97,42 @@ void handle404(HTTPRequest * req, HTTPResponse * res) { // In the create function of the handler, we create a new Handler and keep track // of it using the activeClients array -WebsocketHandler * ChatHandler::create() { +WebsocketHandler * ChatHandler::create() +{ Serial.println("Creating new chat client!"); ChatHandler * handler = new ChatHandler(); - for(int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] == nullptr) { + + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (activeClients[i] == nullptr) + { activeClients[i] = handler; break; } } + return handler; } // When the websocket is closing, we remove the client from the array -void ChatHandler::onClose() { - for(int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] == this) { +void ChatHandler::onClose() +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (activeClients[i] == this) + { + ChatHandler* client = activeClients[i]; activeClients[i] = nullptr; + delete client; + break; } } } // Finally, passing messages around. If we receive something, we send it to all // other clients -void ChatHandler::onMessage(WebsocketInputStreambuf * inbuf) { +void ChatHandler::onMessage(WebsocketInputStreambuf * inbuf) +{ // Get the input message std::ostringstream ss; std::string msg; @@ -181,17 +140,18 @@ void ChatHandler::onMessage(WebsocketInputStreambuf * inbuf) { msg = ss.str(); // Send it back to every client - for(int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] != nullptr) { + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (activeClients[i] != nullptr) + { activeClients[i]->send(msg, SEND_TYPE_TEXT); } } } - - // The following HTML code will present the chat interface. -void handleRoot(HTTPRequest * req, HTTPResponse * res) { +void handleRoot(HTTPRequest * req, HTTPResponse * res) +{ res->setHeader("Content-Type", "text/html"); res->print( @@ -291,3 +251,68 @@ void handleRoot(HTTPRequest * req, HTTPResponse * res) { "\n" ); } + +void setup() +{ + // Initialize the slots + for (int i = 0; i < MAX_CLIENTS; i++) activeClients[i] = nullptr; + + // For logging + Serial.begin(115200); + while (!Serial && millis() < 5000); + + /////////////////////////////////////////////// + + Serial.print("\nStarting Websocket_Chat on "); Serial.println(ARDUINO_BOARD); + + // Connect to WiFi + Serial.println("Setting up WiFi"); + WiFi.begin(WIFI_SSID, WIFI_PSK); + + while (WiFi.status() != WL_CONNECTED) + { + Serial.print("."); + delay(500); + } + + Serial.print("Connected. IP="); + Serial.println(WiFi.localIP()); + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode * node404 = new ResourceNode("", "GET", &handle404); + + // Add the root node to the server + secureServer.registerNode(nodeRoot); + + // The websocket handler can be linked to the server by using a WebsocketNode: + // (Note that the standard defines GET as the only allowed method here, + // so you do not need to pass it explicitly) + WebsocketNode * chatNode = new WebsocketNode("/chat", &ChatHandler::create); + + // Adding the node to the server works in the same way as for all other nodes + secureServer.registerNode(chatNode); + + // Finally, add the 404 not found node to the server. + // The path is ignored for the default node. + secureServer.setDefaultNode(node404); + + Serial.println("Starting server..."); + secureServer.start(); + + if (secureServer.isRunning()) + { + Serial.print("Server ready. Open the following URL in multiple browser windows to start chatting: https://"); + Serial.println(WiFi.localIP()); + } +} + +void loop() +{ + // This call will let the server do its work + secureServer.loop(); + + // Other code would go here... + delay(1); +} diff --git a/examples/Websocket-Chat/cert.h b/examples/Websocket-Chat/cert.h new file mode 100644 index 0000000..c4b5c43 --- /dev/null +++ b/examples/Websocket-Chat/cert.h @@ -0,0 +1,4 @@ +#ifndef CERT_H_ +#define CERT_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/examples/Websocket-Chat/private_key.h b/examples/Websocket-Chat/private_key.h new file mode 100644 index 0000000..ec2498e --- /dev/null +++ b/examples/Websocket-Chat/private_key.h @@ -0,0 +1,4 @@ +#ifndef PRIVATE_KEY_H_ +#define PRIVATE_KEY_H_ + #error You have to run the srcipt extras/create_cert.sh to recreate these files +#endif diff --git a/library.json b/library.json index 0456c47..9b45e5d 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "esp32_https_server", - "keywords": "communication, esp32, http, https, server, ssl, tls, webserver, websockets", + "keywords": "communication, esp32, http, https, server, ssl, tls, webserver, websockets, wt32-eth01", "description": "Alternative ESP32 Webserver implementation for the ESP32, supporting HTTPS and HTTP. The library provides TLS support and simultaneous connections. It can be used to run an HTTP or HTTPS server, or both in parallel. The server's resources are defined through handler and middleware functions, giving an easy start to everyone who has worked with frameworks like Express or Servlets before.", "repository": { @@ -8,7 +8,7 @@ "url": "https://github.com/fhessel/esp32_https_server.git" }, "license": "MIT", - "version": "1.0.0", + "version": "1.1.0", "frameworks": "arduino", "platforms": ["espressif32"] } diff --git a/library.properties b/library.properties index 3ffb202..e603bee 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,11 @@ name=ESP32_HTTPS_Server -version=1.0.0 -author=Frank Hessel -maintainer=Frank Hessel -sentence=Alternative ESP32 Webserver implementation for the ESP32, supporting HTTPS and HTTP. +version=1.1.0 +author=Frank Hessel ,Khoi Hoang +maintainer=Khoi Hoang +sentence=Alternative ESP32 Webserver implementation for the ESP32 and WT32-ETH01, supporting HTTPS and HTTP. paragraph=The library provides TLS support and simultaneous connections. It can be used to run an HTTP or HTTPS server, or both in parallel. The server's resources are defined through handler and middleware functions, giving an easy start to everyone who has worked with frameworks like Express or Servlets before. category=Communication -url=https://github.com/fhessel/esp32_https_server +url=https://github.com/khoih-prog/esp32_https_server architectures=esp32 +depends=WebServer_WT32_ETH01 includes=HTTPSServer.hpp,HTTPRequest.hpp,HTTPResponse.hpp diff --git a/src/HTTPConnection.cpp b/src/HTTPConnection.cpp index 0ab739c..75f15ef 100644 --- a/src/HTTPConnection.cpp +++ b/src/HTTPConnection.cpp @@ -137,8 +137,10 @@ void HTTPConnection::closeConnection() { _httpHeaders = NULL; } - if (_wsHandler != nullptr) { + if (_wsHandler != nullptr) + { HTTPS_LOGD("Free WS Handler"); + _wsHandler->onClose(); delete _wsHandler; _wsHandler = NULL; } @@ -206,7 +208,7 @@ int HTTPConnection::updateBuffer() { } else { // An error occured _connectionState = STATE_ERROR; - HTTPS_LOGE("An receive error occured, FID=%d", _socket); + HTTPS_LOGE("A receive error occurred, FID=%d, code=%d", _socket, readReturnCode); closeConnection(); return -1; } @@ -252,9 +254,13 @@ size_t HTTPConnection::readBuffer(byte* buffer, size_t length) { return length; } -size_t HTTPConnection::pendingBufferSize() { +size_t HTTPConnection::pendingBufferSize() +{ updateBuffer(); - + + if (isClosed()) + return 0; + return _bufferUnusedIdx - _bufferProcessed + pendingByteCount(); } @@ -588,17 +594,21 @@ void HTTPConnection::loop() { } // If the handler has terminated the connection, clean up and close the socket too - if (_wsHandler->closed() || _clientState == CSTATE_CLOSED) { - HTTPS_LOGI("WS closed, freeing Handler, FID=%d", _socket); - delete _wsHandler; - _wsHandler = nullptr; - _connectionState = STATE_CLOSING; + if (_wsHandler != nullptr){ + if (_wsHandler->closed() || _clientState == CSTATE_CLOSED) { + HTTPS_LOGI("WS closed, freeing Handler, FID=%d", _socket); + delete _wsHandler; + _wsHandler = nullptr; + _connectionState = STATE_CLOSING; + } + } else { + HTTPS_LOGI("WS closed due to SSL level issue and cleanded up"); } break; - default:; + default: + break; } } - } diff --git a/src/HTTPConnection.hpp b/src/HTTPConnection.hpp index fb15d7a..60e78b4 100644 --- a/src/HTTPConnection.hpp +++ b/src/HTTPConnection.hpp @@ -6,7 +6,12 @@ #include #include -#include + +//KH mod +//#include +#include +////// + #include // Required for sockets diff --git a/src/HTTPSCallbackFunction.hpp b/src/HTTPSCallbackFunction.hpp index 1329e38..8d37456 100644 --- a/src/HTTPSCallbackFunction.hpp +++ b/src/HTTPSCallbackFunction.hpp @@ -8,7 +8,7 @@ namespace httpsserver { /** * \brief A callback function that will be called by the server to handle a request */ - typedef void (HTTPSCallbackFunction)(HTTPRequest * req, HTTPResponse * res); + typedef void (HTTPSCallbackFunction)(HTTPRequest * req, HTTPResponse * res); } #endif /* SRC_HTTPSCALLBACKFUNCTION_HPP_ */ diff --git a/src/HTTPSConnection.cpp b/src/HTTPSConnection.cpp index e0e3dd0..481770d 100644 --- a/src/HTTPSConnection.cpp +++ b/src/HTTPSConnection.cpp @@ -108,8 +108,16 @@ size_t HTTPSConnection::writeBuffer(byte* buffer, size_t length) { return SSL_write(_ssl, buffer, length); } -size_t HTTPSConnection::readBytesToBuffer(byte* buffer, size_t length) { - return SSL_read(_ssl, buffer, length); +size_t HTTPSConnection::readBytesToBuffer(byte* buffer, size_t length) +{ + int ret = SSL_read(_ssl, buffer, length); + + if (ret < 0) + { + HTTPS_LOGD("SSL_read error: %d", SSL_get_error(_ssl, ret)); + } + + return ret; } size_t HTTPSConnection::pendingByteCount() { diff --git a/src/ResourceNode.hpp b/src/ResourceNode.hpp index c7c4519..3a091da 100644 --- a/src/ResourceNode.hpp +++ b/src/ResourceNode.hpp @@ -10,7 +10,7 @@ namespace httpsserver { /** * \brief This HTTPNode represents a route that maps to a regular HTTP request for a resource (static or dynamic) - * + * * It therefore contrasts to the WebsocketNode, which handles requests for Websockets. */ class ResourceNode : public HTTPNode {