Merge pull request #585 from iranl/psychichttp2

PsychicHTTP v2-dev branch, add HTTPS server (PSRAM only), add HTTP Digest Authentication
This commit is contained in:
iranl
2025-01-01 19:38:50 +01:00
committed by GitHub
127 changed files with 6088 additions and 5226 deletions

View File

@@ -52,20 +52,16 @@ See the "[Connecting via Ethernet](#connecting-via-ethernet-optional)" section f
## Recommended ESP32 devices ## Recommended ESP32 devices
- If WIFI6 is absolutely required: ESP32-C6 We don't recommend using single-core ESP32 devices (ESP32-C3, ESP32-C6, ESP32-H2, ESP32-Solo1).<br>
- If PoE is required: Any of the above mentioned devices with PoE or any other ESP device in combination with a SPI Ethernet module ([W5500](https://www.aliexpress.com/w/wholesale-w5500.html)) and [PoE to Ethernet and USB type B/C splitter](https://aliexpress.com/w/wholesale-poe-splitter-usb-c.html) Although NukiHub supports single-core devices, NukiHub uses both CPU cores (if available) to process tasks (e.g. HTTP server/MQTT client/BLE scanner/BLE client) and thus runs much better on dual-core devices.<br>
- If you want maximum performance and intend to run any or multiple of the following:
- a Nuki Lock and Nuki Opener and/or
- MQTT SSL and/or
- HTTP SSL and/or
- large amounts of keypad codes, timecontrol or authorization entries
- Developing/debugging Nuki devices and/or Nuki Hub
An ESP32-S3 with 2MB of PSRAM or more (look for an ESP32-S3 with the designation N>=4 and R>=2 such as an ESP32-S3 N16R8) When buying a new device in 2025 we can only recommend the ESP32-S3 with PSRAM (look for an ESP32-S3 with the designation N>=4 and R>=2 such as an ESP32-S3 N16R8).<br>
The ESP32-S3 is a dual-core CPU with many GPIO's, ability to enlarge RAM using PSRAM, ability to connect Ethernet modules over SPI and optionally power the device with a PoE splitter.<br>
- In general when buying a new device when size and a couple of dollars more or less are not an issue: An ESP32-S3 with 2MB of PSRAM or more.<br> The only functions missing from the ESP32-S3 as compared to other ESP devices is the ability to use some Ethernet modules only supported by the original ESP32 (and ESP32-P4) and the ability to connect over WIFI6 (C6 or ESP32-P4 with C6 module)
The ESP32-S3 is a dual-core CPU with many GPIO's, ability to enlarge RAM using PSRAM, ability to connect Ethernet modules over SPI and optionally power the device with a PoE splitter. The only functions missing from the ESP32-S3 as compared to other ESP devices is the ability to use some Ethernet modules only supported by the original ESP32 and the ability to connect over WIFI6 (C6) Other considerations:
- If PoE is required: An ESP32-S3 with PSRAM in combination with a SPI Ethernet module ([W5500](https://www.aliexpress.com/w/wholesale-w5500.html)) and [PoE to Ethernet and USB type B/C splitter](https://aliexpress.com/w/wholesale-poe-splitter-usb-c.html) or the M5stack Atom S3R with the M5stack AtomPoe W5500 module
- If WIFI6 is absolutely required (it probably isn't): ESP32-C6
## Feature comparison ## Feature comparison
@@ -194,6 +190,8 @@ In a browser navigate to the IP address assigned to the ESP32.
- RSSI Publish interval: Set to a positive integer to set the amount of seconds between updates to the maintenance/wifiRssi MQTT topic with the current Wi-Fi RSSI, set to -1 to disable, default 60. - RSSI Publish interval: Set to a positive integer to set the amount of seconds between updates to the maintenance/wifiRssi MQTT topic with the current Wi-Fi RSSI, set to -1 to disable, default 60.
- Restart on disconnect: Enable to restart the Nuki Hub when disconnected from the network. - Restart on disconnect: Enable to restart the Nuki Hub when disconnected from the network.
- Check for Firmware Updates every 24h: Enable to allow the Nuki Hub to check the latest release of the Nuki Hub firmware on boot and every 24 hours. Requires the Nuki Hub to be able to connect to github.com. The latest version will be published to MQTT and will be visible on the main page of the Web Configurator. - Check for Firmware Updates every 24h: Enable to allow the Nuki Hub to check the latest release of the Nuki Hub firmware on boot and every 24 hours. Requires the Nuki Hub to be able to connect to github.com. The latest version will be published to MQTT and will be visible on the main page of the Web Configurator.
- HTTP SSL Certificate (PSRAM enabled devices only): Optionally set to the SSL certificate of the HTTPS server, see the "[HTTPS Server](#https-server-optional-psram-enabled-devices-only)" section of this README.
- HTTP SSL Key (PSRAM enabled devices only): Optionally set to the SSL key of the HTTPS server, see the "[HTTPS Server](#https-server-optional-psram-enabled-devices-only)" section of this README.
#### IP Address assignment #### IP Address assignment
@@ -274,8 +272,9 @@ In a browser navigate to the IP address assigned to the ESP32.
#### Credentials #### Credentials
- User: Pick a username to enable HTTP Basic authentication for the Web Configuration, Set to "#" to disable authentication. - User: Pick a username to enable HTTP authentication for the Web Configuration, Set to "#" to disable authentication.
- Password/Retype password: Pick a password to enable HTTP Basic authentication for the Web Configuration. - Password/Retype password: Pick a password to enable HTTP authentication for the Web Configuration.
- Use Digest Authentication (more secure): Enable to use HTTP Digest Authentication instead of HTTP Basic Authentication. Digest authentication is more secure, especially over unencrypted (HTTP) connections.
#### Nuki Lock PIN / Nuki Opener PIN #### Nuki Lock PIN / Nuki Opener PIN
@@ -574,6 +573,23 @@ openssl req -new -key server.key -out server.csr -subj "/C=US/ST=YourState/L=You
# sign it # sign it
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650
``` ```
## HTTPS Server (optional, PSRAM enabled devices only)
The Webconfigurator can use/force HTTPS on PSRAM enabled devices.<br>
To enable SSL encryption, supply the certificate and key in the Network configuration page and reboot NukiHub.<br>
Example self-signed certificate creation for your HTTPS server:
```console
# make a Certificate and key pair, MAKE SURE THE CN MATCHES THE DOMAIN AT WHICH NUKIHUB IS AVAILABLE
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout nukihub.key -out nukihub.crt -subj "/C=US/ST=YourState/L=YourCity/O=YourOrganization/OU=YourUnit/CN=YourCAName"
```
Although you can use the HTTPS server in this way your client device will not trust the certificate by default.
An option would be to configure a proxy SSL server (such as Caddy, Traefik, nginx) with a non-self signed (e.g. let's encrypt) SSL certificate and have this proxy server connect to NukiHub over SSL and trust the self-signed NukiHub certificate for this connection.
## Home Assistant Discovery (optional) ## Home Assistant Discovery (optional)
This software supports [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/) for integrating Nuki Hub with Home Assistant.<br> This software supports [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/) for integrating Nuki Hub with Home Assistant.<br>

View File

@@ -0,0 +1,25 @@
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignConsecutiveMacros: true
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortIfStatementsOnASingleLine: false
BinPackArguments: false
ColumnLimit: 0
ContinuationIndentWidth: 2
FixNamespaceComments: false
IndentAccessModifiers: true
IndentCaseLabels: true
IndentPPDirectives: BeforeHash
IndentWidth: 2
NamespaceIndentation: All
PointerAlignment: Left
ReferenceAlignment: Left
TabWidth: 2
UseTab: Never
BreakBeforeBraces: Linux
AllowShortLambdasOnASingleLine: All
AlignAfterOpenBracket: DontAlign

55
lib/PsychicHttp/.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
_secret.h
.$request flow.drawio.bkp
.$request flow.drawio.dtmp
.clang_complete
.gcc-flags.json
.pio
.pioenvs
.pioenvs
.piolibdeps
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/ipch
.vscode/launch.json
.vscode/settings.json
*.a
*.app
*.dll
*.dylib
*.exe
*.gch
*.la
*.lai
*.lib
*.lo
*.mod
*.o
*.obj
*.opensdf
*.out
*.pch
*.sdf
*.slo
*.so
*.suo
**.DS_Store
**.pio
**.vscode
/build
/psychic-http-loadtest.log
/psychic-websocket-loadtest.log
benchmark/_psychic-http-loadtest.json
benchmark/.~lock.comparison.ods#
benchmark/http-loadtest-results.csv
benchmark/node_modules
benchmark/package-lock.json
benchmark/psychic-http-loadtest.log
benchmark/psychic-websocket-loadtest.json
benchmark/psychic-websocket-loadtest.log
benchmark/websocket-loadtest-results.csv
examples/arduino/
examples/platformio/lib/PsychicHttp
examples/websockets/lib/PsychicHttp
src/cookie.txt
src/secret.h
Visual\ Micro

14
lib/PsychicHttp/.gitmodules vendored Normal file
View File

@@ -0,0 +1,14 @@
[submodule "examples/esp-idf/components/UrlEncode"]
path = examples/esp-idf/components/UrlEncode
url = https://github.com/dzungpv/UrlEncode
[submodule "examples/esp-idf/components/ArduinoJson"]
path = examples/esp-idf/components/ArduinoJson
url = https://github.com/bblanchon/ArduinoJson
branch = 7.x
[submodule "examples/esp-idf/components/arduino-esp32"]
path = examples/esp-idf/components/arduino-esp32
url = https://github.com/espressif/arduino-esp32
branch = idf-release/v4.4
[submodule "examples/esp-idf/components/esp_littlefs"]
path = examples/esp-idf/components/esp_littlefs
url = https://github.com/joltwallet/esp_littlefs.git

View File

@@ -1,3 +1,23 @@
# v2.0
* Modified the request handling to bring initial url matching and filtering into PsychicHttpServer itself.
* Fixed a bug with filter() where endpoint is matched, but filter fails and it doesn't continue matching further endpoints on same uri (checks were in different codebases)
* HTTP_ANY support
* unlimited endpoints (no more need to manually set config.max_uri_handlers)
* much more flexibility for future
* Endpoint Matching Updates
* Endpoint matching functions can be set on server level (```server.setURIMatchFunction()```) or endpoint level (```endpoint.setURIMatchFunction()```)
* Added convenience macros MATCH_SIMPLE, MATCH_WILDCARD, and MATCH_REGEX
* Added regex matching of URIs, enable it with define PSY_ENABLE_REGEX
* On regex matched requests, you can get match data with request->getRegexMatches()
* Ported URL rewrite functionality from ESPAsyncWS
## Changes required from v1.x to v2.0:
* add a ```server.begin()``` or ```server.start()``` after all your ```server.on()``` calls
* remove any calls to ```config.max_uri_handlers```
* if you are using a custom ```server.config.uri_match_fn``` to match uris, change it to ```server.setURIMatchFunction()```
# v1.2.1 # v1.2.1
* Fix bug with missing include preventing the HTTPS server from compiling. * Fix bug with missing include preventing the HTTPS server from compiling.

View File

@@ -23,7 +23,7 @@ PsychicHttp is a webserver library for ESP32 + Arduino framework which uses the
## Differences from ESPAsyncWebserver ## Differences from ESPAsyncWebserver
* No templating system (anyone actually use this?) * No templating system (anyone actually use this?)
* No url rewriting (but you can use request->redirect) * No url rewriting (but you can use response->redirect)
# Usage # Usage
@@ -120,7 +120,7 @@ If you have existing code using ESPAsyncWebserver, you will feel right at home w
## setup() Stuff ## setup() Stuff
* no more server.begin(), call server.listen(80), before you add your handlers * add your handlers and call server.begin()
* server has a configurable limit on .on() endpoints. change it with ```server.config.max_uri_handlers = 20;``` as needed. * server has a configurable limit on .on() endpoints. change it with ```server.config.max_uri_handlers = 20;``` as needed.
* check your callback function definitions: * check your callback function definitions:
* AsyncWebServerRequest -> PsychicRequest * AsyncWebServerRequest -> PsychicRequest
@@ -136,7 +136,7 @@ If you have existing code using ESPAsyncWebserver, you will feel right at home w
## Requests / Responses ## Requests / Responses
* request->send is now request->reply() * request->send is now response->send()
* if you create a response, call response->send() directly, not request->send(reply) * if you create a response, call response->send() directly, not request->send(reply)
* request->headers() is not supported by ESP-IDF, you have to just check for the header you need. * request->headers() is not supported by ESP-IDF, you have to just check for the header you need.
* No AsyncCallbackJsonWebHandler (for now... can add if needed) * No AsyncCallbackJsonWebHandler (for now... can add if needed)
@@ -164,9 +164,6 @@ void setup()
//connect to wifi //connect to wifi
//start the server listening on port 80 (standard HTTP port)
server.listen(80);
//call server methods to attach endpoints and handlers //call server methods to attach endpoints and handlers
server.on(...); server.on(...);
server.serveStatic(...); server.serveStatic(...);
@@ -198,7 +195,7 @@ The ```server.on(...)``` returns a pointer to the endpoint, which can be used to
```cpp ```cpp
//respond to /url only from requests to the AP //respond to /url only from requests to the AP
server.on("/url", HTTP_GET, request_callback)->setFilter(ON_AP_FILTER); server.on("/url", HTTP_GET, request_callback)->addFilter(ON_AP_FILTER);
//require authentication on /url //require authentication on /url
server.on("/url", HTTP_GET, request_callback)->setAuthentication("user", "pass"); server.on("/url", HTTP_GET, request_callback)->setAuthentication("user", "pass");
@@ -212,7 +209,7 @@ server.on("/ws")->attachHandler(&websocketHandler);
The ```PsychicWebHandler``` class is for handling standard web requests. It provides a single callback: ```onRequest()```. This callback is called when the handler receives a valid HTTP request. The ```PsychicWebHandler``` class is for handling standard web requests. It provides a single callback: ```onRequest()```. This callback is called when the handler receives a valid HTTP request.
One major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request. The ```response->reply()``` and ```request->send()``` functions will return this. It is a good habit to return the result of these functions as sending the response will close the connection. One major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request. The ```response->send()``` and ```request->send()``` functions will return this. It is a good habit to return the result of these functions as sending the response will close the connection.
The function definition for the onRequest callback is: The function definition for the onRequest callback is:
@@ -226,7 +223,7 @@ Here is a simple example that sends back the client's IP on the URL /ip
server.on("/ip", [](PsychicRequest *request) server.on("/ip", [](PsychicRequest *request)
{ {
String output = "Your IP is: " + request->client()->remoteIP().toString(); String output = "Your IP is: " + request->client()->remoteIP().toString();
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
``` ```
@@ -294,7 +291,7 @@ It's worth noting that there is no standard way of passing in a filename for thi
String url = "/" + request->getFilename(); String url = "/" + request->getFilename();
String output = "<a href=\"" + url + "\">" + url + "</a>"; String output = "<a href=\"" + url + "\">" + url + "</a>";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//wildcard basic file upload - POST to /upload/filename.ext //wildcard basic file upload - POST to /upload/filename.ext
@@ -352,7 +349,7 @@ Very similar to the basic upload, with 2 key differences:
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//upload to /multipart url //upload to /multipart url
@@ -374,11 +371,11 @@ The ```server.serveStatic()``` function handles creating the handler and assigni
```cpp ```cpp
//serve static files from LittleFS/www on / only to clients on same wifi network //serve static files from LittleFS/www on / only to clients on same wifi network
//this is where our /index.html file lives //this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); server.serveStatic("/", LittleFS, "/www/")->addFilter(ON_STA_FILTER);
//serve static files from LittleFS/www-ap on / only to clients on SoftAP //serve static files from LittleFS/www-ap on / only to clients on SoftAP
//this is where our /index.html file lives //this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER);
//serve static files from LittleFS/img on /img //serve static files from LittleFS/img on /img
//it's more efficient to serve everything from a single www directory, but this is also possible. //it's more efficient to serve everything from a single www directory, but this is also possible.
@@ -426,17 +423,17 @@ Here is a basic example of using WebSockets:
PsychicWebSocketHandler websocketHandler(); PsychicWebSocketHandler websocketHandler();
websocketHandler.onOpen([](PsychicWebSocketClient *client) { websocketHandler.onOpen([](PsychicWebSocketClient *client) {
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str());
client->sendMessage("Hello!"); client->sendMessage("Hello!");
}); });
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload);
return request->reply(frame); return response->send(frame);
}); });
websocketHandler.onClose([](PsychicWebSocketClient *client) { websocketHandler.onClose([](PsychicWebSocketClient *client) {
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str());
}); });
//attach the handler to /ws. You can then connect to ws://ip.address/ws //attach the handler to /ws. You can then connect to ws://ip.address/ws
@@ -452,7 +449,7 @@ The onFrame() callback has 2 parameters:
For sending data on the websocket connection, there are 3 methods: For sending data on the websocket connection, there are 3 methods:
* ```request->reply()``` - only available in the onFrame() callback context. * ```response->send()``` - only available in the onFrame() callback context.
* ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients. * ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients.
* ```client->send()``` - can be used anywhere* to send a websocket message to a specific client * ```client->send()``` - can be used anywhere* to send a websocket message to a specific client
@@ -488,12 +485,12 @@ Here is a basic example of using PsychicEventSource:
PsychicEventSource eventSource; PsychicEventSource eventSource;
eventSource.onOpen([](PsychicEventSourceClient *client) { eventSource.onOpen([](PsychicEventSourceClient *client) {
Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str());
client->send("Hello user!", NULL, millis(), 1000); client->send("Hello user!", NULL, millis(), 1000);
}); });
eventSource.onClose([](PsychicEventSourceClient *client) { eventSource.onClose([](PsychicEventSourceClient *client) {
Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString()); Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str());
}); });
//attach the handler to /events //attach the handler to /events
@@ -524,7 +521,7 @@ PsychicHttp supports HTTPS / SSL out of the box, however there are some limitati
#include <PsychicHttp.h> #include <PsychicHttp.h>
#include <PsychicHttpsServer.h> #include <PsychicHttpsServer.h>
PsychicHttpsServer server; PsychicHttpsServer server;
server.listen(443, server_cert, server_key); server.setCertificate(server_cert, server_key);
``` ```
```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively. ```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively.
@@ -552,10 +549,9 @@ Last, but not least, you can create a separate HTTP server on port 80 that redir
//this creates a 2nd server listening on port 80 and redirects all requests HTTPS //this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer *redirectServer = new PsychicHttpServer(); PsychicHttpServer *redirectServer = new PsychicHttpServer();
redirectServer->config.ctrl_port = 20420; // just a random port different from the default one redirectServer->config.ctrl_port = 20420; // just a random port different from the default one
redirectServer->listen(80);
redirectServer->onNotFound([](PsychicRequest *request) { redirectServer->onNotFound([](PsychicRequest *request) {
String url = "https://" + request->host() + request->url(); String url = "https://" + request->host() + request->url();
return request->redirect(url.c_str()); return response->redirect(url.c_str());
}); });
``` ```
@@ -775,51 +771,22 @@ With all due respect to @me-no-dev who has done some amazing work in the open so
ArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well. Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones. ArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well. Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones.
# Community / Support
The best way to get support is probably with Github issues. There is also a [Discord chat](https://discord.gg/CM5abjGG) that is pretty active.
# Roadmap # Roadmap
## v1.2: ESPAsyncWebserver Parity ## v2.0: ESPAsyncWebserver Parity
Change:
Modify the request handling to bring initail url matching and filtering into PsychicHttpServer itself.
Benefits:
* Fix a bug with filter() where endpoint is matched, but filter fails and it doesn't continue matching further endpoints (checks are in different codebases)
* HTTP_ANY support
* unlimited endpoints
* we would use a List to store endpoints
* dont have to pre-declare config.max_uri_handlers;
* much more flexibility for future
Issues
* it would log a warning on every request as if its a 404. (httpd_uri.c:298)
* req->user_ctx is not passed in. (httpd_uri.c:309)
* but... user_ctx is something we could store in the psychicendpoint data
* Websocket support assumes an endpoint with matching url / method (httpd_uri.c:312)
* we could copy and bring this code into our own internal request processor
* would need to manually maintain more code (~100 lines?) and be more prone to esp-idf http_server updates causing problems.
How to implement
* set config.max_uri_handlers = 1;
* possibly do not register any uri_handlers (looks like it would be fastest way to exit httpd_find_uri_handler (httpd_uri.c:94))
* looks like 404 is set by default, so should work.
* modify PsychicEndpoint to store the stuff we would pass to http_server
* create a new function handleRequest() before PsychicHttpServer::defaultNotFoundHandler to process incoming requests.
* bring in code from PsychicHttpServer::notFoundHandler
* add new code to loop over endpoints to call match and filter
* bring code from esp-idf library
* templating system
* regex url matching
* rewrite urls?
* What else are we missing?
* As much ESPAsyncWebServer compatibility as possible
* Update benchmarks and get new data
* we should also track program size and memory usage
## Longterm Wants ## Longterm Wants
* investigate websocket performance gap * investigate websocket performance gap
* support for esp-idf framework * support for esp-idf framework
* support for arduino 3.0 framework
* Enable worker based multithreading with esp-idf v5.x * Enable worker based multithreading with esp-idf v5.x
* 100-continue support? * 100-continue support?

View File

@@ -1,6 +1,8 @@
* Update CHANGELOG * Update CHANGELOG
* Bump version in src/PsychicVersion.h
* Bump version in library.json * Bump version in library.json
* Bump version in library.properties * Bump version in library.properties
* Make new release + tag * Make new release + tag
* this will get pulled in automatically by Arduino Library Indexer * this will get pulled in automatically by Arduino Library Indexer
* run ```pio pkg publish``` to publish to Platform.io * ~~run ```pio pkg publish``` to publish to Platform.io~~
* automatically publishes on release via .github hook

View File

@@ -8,18 +8,18 @@
*/ */
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h> #include <ArduinoJSON.h>
#include <LittleFS.h>
#include <MongooseCore.h> #include <MongooseCore.h>
#include <MongooseHttpServer.h> #include <MongooseHttpServer.h>
#include <LittleFS.h> #include <WiFi.h>
#include <ArduinoJSON.h>
const char *ssid = ""; const char* ssid = "";
const char *password = ""; const char* password = "";
MongooseHttpServer server; MongooseHttpServer server;
const char *htmlContent = R"( const char* htmlContent = R"(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -154,25 +154,23 @@ void setup()
// To debug, please enable Core Debug Level to Verbose // To debug, please enable Core Debug Level to Verbose
if (connectToWifi()) if (connectToWifi())
{ {
if(!LittleFS.begin()) if (!LittleFS.begin())
{ {
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
//start our server // start our server
Mongoose.begin(); Mongoose.begin();
server.begin(80); server.begin(80);
//index file // index file
server.on("/", HTTP_GET, [](MongooseHttpServerRequest *request) server.on("/", HTTP_GET, [](MongooseHttpServerRequest* request)
{ { request->send(200, "text/html", htmlContent); });
request->send(200, "text/html", htmlContent);
});
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/api", HTTP_GET, [](MongooseHttpServerRequest *request) server.on("/api", HTTP_GET, [](MongooseHttpServerRequest* request)
{ {
//create a response object //create a response object
StaticJsonDocument<128> output; StaticJsonDocument<128> output;
output["msg"] = "status"; output["msg"] = "status";
@@ -189,19 +187,18 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
request->send(200, "application/json", jsonBuffer.c_str()); request->send(200, "application/json", jsonBuffer.c_str()); });
});
//websocket // websocket
server.on("/ws$")-> server.on("/ws$")->onFrame([](MongooseHttpWebSocketConnection* connection, int flags, uint8_t* data, size_t len)
onFrame([](MongooseHttpWebSocketConnection *connection, int flags, uint8_t *data, size_t len) { {
connection->send(WEBSOCKET_OP_TEXT, data, len); connection->send(WEBSOCKET_OP_TEXT, data, len);
//server.sendAll(connection, (char *)data); // server.sendAll(connection, (char *)data);
}); });
//hack - no servestatic // hack - no servestatic
server.on("/alien.png", HTTP_GET, [](MongooseHttpServerRequest *request) server.on("/alien.png", HTTP_GET, [](MongooseHttpServerRequest* request)
{ {
//open our file //open our file
File fp = LittleFS.open("/www/alien.png"); File fp = LittleFS.open("/www/alien.png");
size_t length = fp.size(); size_t length = fp.size();
@@ -223,8 +220,7 @@ void setup()
free(data); free(data);
} }
else else
request->send(503); request->send(503); });
});
} }
} }

View File

@@ -11,12 +11,31 @@
[env] [env]
platform = espressif32 platform = espressif32
framework = arduino framework = arduino
board = esp32dev ; board = esp32dev
board = esp32-s3-devkitc-1
upload_port = /dev/ttyACM0
monitor_port = /dev/ttyACM1
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
lib_deps =
https://github.com/me-no-dev/ESPAsyncWebServer lib_compat_mode = strict
bblanchon/ArduinoJson lib_ldf_mode = chain
lib_deps =
; mathieucarbou/AsyncTCP @ 3.2.10
https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
mathieucarbou/ESPAsyncWebServer @ 3.3.16
bblanchon/ArduinoJson
lib_ignore =
AsyncTCP
mathieucarbou/AsyncTCP
board_build.filesystem = littlefs board_build.filesystem = littlefs
build_flags =
-D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000
-D CONFIG_ASYNC_TCP_PRIORITY=10
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=128
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096
-D WS_MAX_QUEUED_MESSAGES=128
[env:default] [env:default]

View File

@@ -7,19 +7,29 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
#include "_secret.h"
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h> #include <ArduinoJson.h>
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <ESPmDNS.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <ArduinoJSON.h> #include <WiFi.h>
const char *ssid = ""; #ifndef WIFI_SSID
const char *password = ""; #error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there."
#endif
// Enter your WIFI credentials in secret.h
const char* ssid = WIFI_SSID;
const char* password = WIFI_PASS;
// hostname for mdns (psychic.local)
const char* local_hostname = "psychic";
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
const char *htmlContent = R"( const char* htmlContent = R"(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -79,14 +89,16 @@ const char *htmlContent = R"(
</html> </html>
)"; )";
const size_t htmlContentLen = strlen(htmlContent);
bool connectToWifi() bool connectToWifi()
{ {
Serial.println(); Serial.println();
Serial.print("[WiFi] Connecting to "); Serial.print("[WiFi] Connecting to ");
Serial.println(ssid); Serial.println(ssid);
WiFi.setSleep(false); // WiFi.setSleep(false);
WiFi.useStaticBuffers(true); // WiFi.useStaticBuffers(true);
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
@@ -95,10 +107,8 @@ bool connectToWifi()
int numberOfTries = 20; int numberOfTries = 20;
// Wait for the WiFi event // Wait for the WiFi event
while (true) while (true) {
{ switch (WiFi.status()) {
switch (WiFi.status())
{
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
Serial.println("[WiFi] SSID not found"); Serial.println("[WiFi] SSID not found");
break; break;
@@ -128,15 +138,12 @@ bool connectToWifi()
} }
delay(tryDelay); delay(tryDelay);
if (numberOfTries <= 0) if (numberOfTries <= 0) {
{
Serial.print("[WiFi] Failed to connect to WiFi!"); Serial.print("[WiFi] Failed to connect to WiFi!");
// Use disconnect function to force stop trying to connect // Use disconnect function to force stop trying to connect
WiFi.disconnect(); WiFi.disconnect();
return false; return false;
} } else {
else
{
numberOfTries--; numberOfTries--;
} }
} }
@@ -144,28 +151,29 @@ bool connectToWifi()
return false; return false;
} }
void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ void onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
if(type == WS_EVT_CONNECT){ {
//client connected if (type == WS_EVT_CONNECT) {
// Serial.printf("ws[%s][%u] connect\n", server->url(), client->id()); // client connected
// client->printf("Hello Client %u :)", client->id()); // Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
// client->ping(); // client->printf("Hello Client %u :)", client->id());
} else if(type == WS_EVT_DISCONNECT){ // client->ping();
//client disconnected } else if (type == WS_EVT_DISCONNECT) {
// Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); // client disconnected
} else if(type == WS_EVT_ERROR){ // Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id());
//error was received from the other end } else if (type == WS_EVT_ERROR) {
// Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); // error was received from the other end
} else if(type == WS_EVT_PONG){ // Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data);
//pong message was received (in response to a ping request maybe) } else if (type == WS_EVT_PONG) {
// Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); // pong message was received (in response to a ping request maybe)
} else if(type == WS_EVT_DATA){ // Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:"");
//data packet } else if (type == WS_EVT_DATA) {
AwsFrameInfo * info = (AwsFrameInfo*)arg; // data packet
if(info->final && info->index == 0 && info->len == len){ AwsFrameInfo* info = (AwsFrameInfo*)arg;
//the whole message is in a single frame and we got all of it's data if (info->final && info->index == 0 && info->len == len) {
// Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); // the whole message is in a single frame and we got all of it's data
if(info->opcode == WS_TEXT){ // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len);
if (info->opcode == WS_TEXT) {
data[len] = 0; data[len] = 0;
// Serial.printf("%s\n", (char*)data); // Serial.printf("%s\n", (char*)data);
} else { } else {
@@ -174,22 +182,21 @@ void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
// } // }
// Serial.printf("\n"); // Serial.printf("\n");
} }
if(info->opcode == WS_TEXT) if (info->opcode == WS_TEXT) {
{ client->text((char*)data, len);
client->text((char *)data, len);
} }
// else // else
// client->binary("I got your binary message"); // client->binary("I got your binary message");
} else { } else {
//message is comprised of multiple frames or the frame is split into multiple packets // message is comprised of multiple frames or the frame is split into multiple packets
if(info->index == 0){ if (info->index == 0) {
// if(info->num == 0) // if(info->num == 0)
// Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); // Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
// Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); // Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len);
} }
Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT) ? "text" : "binary", info->index, info->index + len);
if(info->message_opcode == WS_TEXT){ if (info->message_opcode == WS_TEXT) {
data[len] = 0; data[len] = 0;
// Serial.printf("%s\n", (char*)data); // Serial.printf("%s\n", (char*)data);
} else { } else {
@@ -199,13 +206,12 @@ void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
// Serial.printf("\n"); // Serial.printf("\n");
} }
if((info->index + len) == info->len){ if ((info->index + len) == info->len) {
// Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); // Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len);
if(info->final){ if (info->final) {
// Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); // Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
if(info->message_opcode == WS_TEXT) if (info->message_opcode == WS_TEXT) {
{ client->text((char*)data, info->len);
client->text((char *)data, info->len);
} }
// else // else
// client->binary("I got your binary message"); // client->binary("I got your binary message");
@@ -223,40 +229,41 @@ void setup()
// We start by connecting to a WiFi network // We start by connecting to a WiFi network
// To debug, please enable Core Debug Level to Verbose // To debug, please enable Core Debug Level to Verbose
if (connectToWifi()) if (connectToWifi()) {
{ // set up our esp32 to listen on the local_hostname.local domain
if(!LittleFS.begin()) if (!MDNS.begin(local_hostname)) {
{ Serial.println("Error starting mDNS");
return;
}
MDNS.addService("http", "tcp", 80);
if (!LittleFS.begin()) {
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
{ // ESPAsyncWebServer, sending a char* does a buffer copy, unlike Psychic.
request->send(200, "text/html", htmlContent); // Sending flash data is done with the uint8_t* overload.
request->send(200, "text/html", (uint8_t*)htmlContent, htmlContentLen);
}); });
//serve static files from LittleFS/www on / // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.serveStatic("/", LittleFS, "/www/"); server.on("/api", HTTP_GET, [](AsyncWebServerRequest* request) {
// create a response object
//api - parameters passed in via query eg. /api/endpoint?foo=bar JsonDocument output;
server.on("/api", HTTP_GET, [](AsyncWebServerRequest *request)
{
//create a response object
StaticJsonDocument<128> output;
output["msg"] = "status"; output["msg"] = "status";
output["status"] = "success"; output["status"] = "success";
output["millis"] = millis(); output["millis"] = millis();
//work with some params // work with some params
if (request->hasParam("foo")) if (request->hasParam("foo")) {
{ const AsyncWebParameter* foo = request->getParam("foo");
AsyncWebParameter* foo = request->getParam("foo");
output["foo"] = foo->value(); output["foo"] = foo->value();
} }
//serialize and return // serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
request->send(200, "application/json", jsonBuffer.c_str()); request->send(200, "application/json", jsonBuffer.c_str());
@@ -265,6 +272,10 @@ void setup()
ws.onEvent(onEvent); ws.onEvent(onEvent);
server.addHandler(&ws); server.addHandler(&ws);
// put this last, otherwise it clogs the other requests
// serve static files from LittleFS/www on /
server.serveStatic("/", LittleFS, "/www/");
server.begin(); server.begin();
} }
} }
@@ -272,5 +283,6 @@ void setup()
void loop() void loop()
{ {
ws.cleanupClients(); ws.cleanupClients();
Serial.printf("Free Heap: %d\n", esp_get_free_heap_size());
delay(1000); delay(1000);
} }

View File

@@ -0,0 +1,2 @@
#define WIFI_SSID "Your_SSID"
#define WIFI_PASS "Your_PASS"

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
//stress test the client opening/closing code
const EventSource = require('eventsource'); const EventSource = require('eventsource');
const url = 'http://192.168.2.131/events'; const url = 'http://psychic.local/events';
async function eventSourceClient() { async function eventSourceClient() {
console.log(`Starting test`); console.log(`Starting test`);

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
//stress test the http request code
const axios = require('axios'); const axios = require('axios');
const url = 'http://192.168.2.131/api'; const url = 'http://psychic.local/api';
const queryParams = { const queryParams = {
foo: 'bar', foo: 'bar',
foo1: 'bar', foo1: 'bar',

View File

@@ -1,37 +1,42 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#Command to install the testers: #Command to install the testers:
# npm install -g autocannon # npm install
TEST_IP="192.168.2.131" TEST_IP="psychic.local"
TEST_TIME=60 TEST_TIME=10
LOG_FILE=psychic-http-loadtest.log #LOG_FILE=psychic-http-loadtest.log
LOG_FILE=_psychic-http-loadtest.json
RESULTS_FILE=http-loadtest-results.csv
TIMEOUT=10000 TIMEOUT=10000
WORKERS=1
PROTOCOL=http PROTOCOL=http
#PROTOCOL=https #PROTOCOL=https
if test -f "$LOG_FILE"; then echo "url,connections,rps,latency,errors" > $RESULTS_FILE
rm $LOG_FILE
fi
for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20 for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20
#for CONCURRENCY in 20
do do
printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/" echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/"
#loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/" --quiet >> $LOG_FILE autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/" > $LOG_FILE
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/" >> $LOG_FILE 2>&1 node parse-http-test.js $LOG_FILE $RESULTS_FILE
printf "\n\n----------------\n\n" >> $LOG_FILE sleep 5
sleep 1 done
for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20
do
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/api" echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/api"
#loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/api?foo=bar" --quiet >> $LOG_FILE autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/api?foo=bar" > $LOG_FILE
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/api?foo=bar" >> $LOG_FILE 2>&1 node parse-http-test.js $LOG_FILE $RESULTS_FILE
printf "\n\n----------------\n\n" >> $LOG_FILE sleep 5
sleep 1 done
for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20
do
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/alien.png" echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/alien.png"
#loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --timeout $TIMEOUT "$PROTOCOL://$TEST_IP/alien.png" --quiet >> $LOG_FILE autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/alien.png" > $LOG_FILE
autocannon -c $CONCURRENCY -w 1 -d $TEST_TIME --renderStatusCodes "$PROTOCOL://$TEST_IP/alien.png" >> $LOG_FILE 2>&1 node parse-http-test.js $LOG_FILE $RESULTS_FILE
printf "\n\n----------------\n\n" >> $LOG_FILE sleep 5
sleep 1 done
done
rm $LOG_FILE

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#Command to install the testers: #Command to install the testers:
# npm install -g loadtest # npm install
TEST_IP="192.168.2.131" TEST_IP="psychic.local"
TEST_TIME=60 TEST_TIME=60
LOG_FILE=psychic-websocket-loadtest.log LOG_FILE=psychic-websocket-loadtest.json
RESULTS_FILE=websocket-loadtest-results.csv
PROTOCOL=ws PROTOCOL=ws
#PROTOCOL=wss #PROTOCOL=wss
@@ -12,20 +13,33 @@ if test -f "$LOG_FILE"; then
rm $LOG_FILE rm $LOG_FILE
fi fi
for CONCURRENCY in 1 2 3 4 5 6 7 echo "url,clients,rps,latency,errors" > $RESULTS_FILE
CORES=1
for CONCURRENCY in 1 2 3 4 5
do do
printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE
echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/ws" echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/ws"
loadtest -c $CONCURRENCY --cores 1 -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE
sleep 1 node parse-websocket-test.js $LOG_FILE $RESULTS_FILE
sleep 2
done done
for CONNECTIONS in 8 10 16 20 CORES=2
#for CONNECTIONS in 20 for CONNECTIONS in 6 8 10 12 14
do do
CONCURRENCY=$((CONNECTIONS / 2)) CONCURRENCY=$((CONNECTIONS / 2))
printf "\n\nCLIENTS: *** $CONNECTIONS ***\n\n" >> $LOG_FILE
echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws" echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws"
loadtest -c $CONCURRENCY --cores 2 -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE
sleep 1 node parse-websocket-test.js $LOG_FILE $RESULTS_FILE
done sleep 2
done
CORES=4
for CONNECTIONS in 16 20 24 28 32
do
CONCURRENCY=$((CONNECTIONS / CORES))
echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws"
loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE
node parse-websocket-test.js $LOG_FILE $RESULTS_FILE
sleep 2
done

View File

@@ -1,7 +1,10 @@
{ {
"dependencies": { "dependencies": {
"autocannon": "^7.15.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"csv-writer": "^1.6.0",
"eventsource": "^2.0.2", "eventsource": "^2.0.2",
"loadtest": "^8.0.9",
"ws": "^8.14.2" "ws": "^8.14.2"
} }
} }

View File

@@ -0,0 +1,55 @@
const fs = require('fs');
const createCsvWriter = require('csv-writer').createObjectCsvWriter;
// Get the input and output file paths from the command line arguments
const inputFilePath = process.argv[2];
const outputFilePath = process.argv[3];
if (!inputFilePath || !outputFilePath) {
console.error('Usage: node script.js <inputFilePath> <outputFilePath>');
process.exit(1);
}
// Read and parse the JSON file
fs.readFile(inputFilePath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading the input file:', err);
return;
}
// Parse the JSON data
const jsonData = JSON.parse(data);
// Extract the desired fields
const { url, connections, latency, requests, errors } = jsonData;
const latencyMean = latency.mean;
const requestsMean = requests.mean;
// Set up the CSV writer
const csvWriter = createCsvWriter({
path: outputFilePath,
header: [
{id: 'url', title: 'URL'},
{id: 'connections', title: 'Connections'},
{id: 'requestsMean', title: 'Requests Mean'},
{id: 'latencyMean', title: 'Latency Mean'},
{id: 'errors', title: 'Errors'},
],
append: true // this will append to the existing file
});
// Prepare the data to be written
const records = [
{ url: url, connections: connections, latencyMean: latencyMean, requestsMean: requestsMean, errors: errors }
];
// Write the data to the CSV file
csvWriter.writeRecords(records)
.then(() => {
console.log('Data successfully appended to CSV file.');
})
.catch(err => {
console.error('Error writing to the CSV file:', err);
});
});

View File

@@ -0,0 +1,64 @@
const fs = require('fs');
const readline = require('readline');
if (process.argv.length !== 4) {
console.error('Usage: node parse-websocket-test.js <input_file> <output_file>');
process.exit(1);
}
const inputFile = process.argv[2];
const outputFile = process.argv[3];
async function parseFile() {
const fileStream = fs.createReadStream(inputFile);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let targetUrl = null;
let totalErrors = null;
let meanLatency = null;
let effectiveRps = null;
let concurrentClients = null;
for await (const line of rl) {
if (line.startsWith('Target URL:')) {
targetUrl = line.split(':').slice(1).join(':').trim();
}
if (line.startsWith('Total errors:')) {
totalErrors = parseInt(line.split(':')[1].trim(), 10);
}
if (line.startsWith('Mean latency:')) {
meanLatency = parseFloat(line.split(':')[1].trim());
}
if (line.startsWith('Effective rps:')) {
effectiveRps = parseInt(line.split(':')[1].trim(), 10);
}
if (line.startsWith('Concurrent clients:')) {
concurrentClients = parseInt(line.split(':')[1].trim(), 10);
}
}
if (targetUrl === null || totalErrors === null || meanLatency === null || effectiveRps === null || concurrentClients === null) {
console.error('Failed to extract necessary data from the input file');
process.exit(1);
}
const csvLine = `${targetUrl},${concurrentClients},${effectiveRps},${meanLatency},${totalErrors}\n`;
fs.appendFile(outputFile, csvLine, (err) => {
if (err) {
console.error('Failed to append to CSV file:', err);
process.exit(1);
}
console.log('Data successfully appended to CSV file.');
});
}
parseFile().catch(err => {
console.error('Error reading file:', err);
process.exit(1);
});

View File

@@ -15,8 +15,14 @@ board = esp32dev
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
lib_deps = lib_deps =
https://github.com/hoeken/PsychicHttp
bblanchon/ArduinoJson bblanchon/ArduinoJson
board_build.filesystem = littlefs board_build.filesystem = littlefs
[env:default] [env:default]
lib_deps = https://github.com/hoeken/PsychicHttp
[env:v2-dev]
lib_deps = https://github.com/hoeken/PsychicHttp#v2-dev
board = esp32-s3-devkitc-1
upload_port = /dev/ttyACM0
monitor_port = /dev/ttyACM1

View File

@@ -7,26 +7,30 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicHttp.h>
#include <LittleFS.h>
#include <ArduinoJSON.h>
#include "_secret.h" #include "_secret.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <PsychicHttp.h>
#include <WiFi.h>
#ifndef WIFI_SSID #ifndef WIFI_SSID
#error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there." #error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there."
#endif #endif
//Enter your WIFI credentials in secret.h // Enter your WIFI credentials in secret.h
const char *ssid = WIFI_SSID; const char* ssid = WIFI_SSID;
const char *password = WIFI_PASS; const char* password = WIFI_PASS;
// hostname for mdns (psychic.local)
const char* local_hostname = "psychic";
PsychicHttpServer server; PsychicHttpServer server;
PsychicWebSocketHandler websocketHandler; PsychicWebSocketHandler websocketHandler;
PsychicEventSource eventSource; PsychicEventSource eventSource;
const char *htmlContent = R"( const char* htmlContent = R"(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -92,8 +96,8 @@ bool connectToWifi()
Serial.print("[WiFi] Connecting to "); Serial.print("[WiFi] Connecting to ");
Serial.println(ssid); Serial.println(ssid);
WiFi.setSleep(false); // WiFi.setSleep(false);
WiFi.useStaticBuffers(true); // WiFi.useStaticBuffers(true);
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
@@ -102,10 +106,8 @@ bool connectToWifi()
int numberOfTries = 20; int numberOfTries = 20;
// Wait for the WiFi event // Wait for the WiFi event
while (true) while (true) {
{ switch (WiFi.status()) {
switch (WiFi.status())
{
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
Serial.println("[WiFi] SSID not found"); Serial.println("[WiFi] SSID not found");
break; break;
@@ -135,15 +137,12 @@ bool connectToWifi()
} }
delay(tryDelay); delay(tryDelay);
if (numberOfTries <= 0) if (numberOfTries <= 0) {
{
Serial.print("[WiFi] Failed to connect to WiFi!"); Serial.print("[WiFi] Failed to connect to WiFi!");
// Use disconnect function to force stop trying to connect // Use disconnect function to force stop trying to connect
WiFi.disconnect(); WiFi.disconnect();
return false; return false;
} } else {
else
{
numberOfTries--; numberOfTries--;
} }
} }
@@ -157,47 +156,42 @@ void setup()
delay(10); delay(10);
Serial.println("PsychicHTTP Benchmark"); Serial.println("PsychicHTTP Benchmark");
if (connectToWifi()) if (connectToWifi()) {
{ // set up our esp32 to listen on the local_hostname.local domain
if(!LittleFS.begin()) if (!MDNS.begin(local_hostname)) {
{ Serial.println("Error starting mDNS");
return;
}
MDNS.addService("http", "tcp", 80);
if (!LittleFS.begin()) {
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
//start our server // our index
server.listen(80); server.on("/", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send(200, "text/html", htmlContent); });
//our index // serve static files from LittleFS/www on /
server.on("/", HTTP_GET, [](PsychicRequest *request)
{
return request->reply(200, "text/html", htmlContent);
});
//serve static files from LittleFS/www on /
server.serveStatic("/", LittleFS, "/www/"); server.serveStatic("/", LittleFS, "/www/");
//a websocket echo server // a websocket echo server
websocketHandler.onOpen([](PsychicWebSocketClient *client) { websocketHandler.onOpen([](PsychicWebSocketClient* client) {
client->sendMessage("Hello!"); // client->sendMessage("Hello!");
});
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
request->reply(frame);
return ESP_OK;
}); });
websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) {
response->send(frame);
return ESP_OK; });
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
//EventSource server // EventSource server
eventSource.onOpen([](PsychicEventSourceClient *client) { eventSource.onOpen([](PsychicEventSourceClient* client) { client->send("Hello", NULL, millis(), 1000); });
client->send("Hello", NULL, millis(), 1000);
});
server.on("/events", &eventSource); server.on("/events", &eventSource);
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/api", HTTP_GET, [](PsychicRequest *request) server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{
//create a response object //create a response object
StaticJsonDocument<128> output; JsonDocument output;
output["msg"] = "status"; output["msg"] = "status";
output["status"] = "success"; output["status"] = "success";
output["millis"] = millis(); output["millis"] = millis();
@@ -212,16 +206,16 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str()); });
});
server.begin();
} }
} }
unsigned long last; unsigned long last;
void loop() void loop()
{ {
if (millis() - last > 1000) if (millis() - last > 1000) {
{
Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); Serial.printf("Free Heap: %d\n", esp_get_free_heap_size());
last = millis(); last = millis();
} }

View File

@@ -7,22 +7,21 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
#include <Arduino.h>
#include <WiFi.h>
#include <PsychicHttp.h>
#include <LittleFS.h>
#include <ArduinoJSON.h>
#include "_secret.h" #include "_secret.h"
#include <Arduino.h>
#include <ArduinoJSON.h>
#include <LittleFS.h>
#include <PsychicHttp.h> #include <PsychicHttp.h>
#include <PsychicHttpsServer.h> #include <PsychicHttpsServer.h>
#include <WiFi.h>
#ifndef WIFI_SSID #ifndef WIFI_SSID
#error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there."
#endif #endif
//Enter your WIFI credentials in secret.h // Enter your WIFI credentials in secret.h
const char *ssid = WIFI_SSID; const char* ssid = WIFI_SSID;
const char *password = WIFI_PASS; const char* password = WIFI_PASS;
PsychicHttpsServer server; PsychicHttpsServer server;
PsychicWebSocketHandler websocketHandler; PsychicWebSocketHandler websocketHandler;
@@ -30,7 +29,7 @@ PsychicWebSocketHandler websocketHandler;
String server_cert; String server_cert;
String server_key; String server_key;
const char *htmlContent = R"( const char* htmlContent = R"(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@@ -162,52 +161,56 @@ void setup()
if (connectToWifi()) if (connectToWifi())
{ {
if(!LittleFS.begin()) if (!LittleFS.begin())
{ {
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
File fp = LittleFS.open("/server.crt"); File fp = LittleFS.open("/server.crt");
if (fp) { if (fp)
{
server_cert = fp.readString(); server_cert = fp.readString();
} else { }
else
{
Serial.println("server.pem not found, SSL not available"); Serial.println("server.pem not found, SSL not available");
return; return;
} }
fp.close(); fp.close();
File fp2 = LittleFS.open("/server.key"); File fp2 = LittleFS.open("/server.key");
if (fp2) { if (fp2)
{
server_key = fp2.readString(); server_key = fp2.readString();
} else { }
else
{
Serial.println("server.key not found, SSL not available"); Serial.println("server.key not found, SSL not available");
return; return;
} }
fp2.close(); fp2.close();
//start our server // start our server
server.listen(443, server_cert.c_str(), server_key.c_str()); server.setCertificate(server_cert.c_str(), server_key.c_str());
//our index // our index
server.on("/", HTTP_GET, [](PsychicRequest *request) server.on("/", HTTP_GET, [](PsychicRequest* request)
{ { return response->send(200, "text/html", htmlContent); });
return request->reply(200, "text/html", htmlContent);
});
//serve static files from LittleFS/www on / // serve static files from LittleFS/www on /
server.serveStatic("/", LittleFS, "/www/"); server.serveStatic("/", LittleFS, "/www/");
//a websocket echo server // a websocket echo server
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame)
request->reply(frame); {
return ESP_OK; response->send(frame);
}); return ESP_OK; });
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/api", HTTP_GET, [](PsychicRequest *request) server.on("/api", HTTP_GET, [](PsychicRequest* request)
{ {
//create a response object //create a response object
StaticJsonDocument<128> output; StaticJsonDocument<128> output;
output["msg"] = "status"; output["msg"] = "status";
@@ -224,8 +227,7 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str()); });
});
} }
} }

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
//stress test the client open/close for websockets
const WebSocket = require('ws'); const WebSocket = require('ws');
const uri = 'ws://192.168.2.131/ws'; const uri = 'ws://psychic.local/ws';
async function websocketClient() { async function websocketClient() {
console.log(`Starting test`); console.log(`Starting test`);

View File

@@ -206,30 +206,25 @@ void setup()
//do we want secure or not? //do we want secure or not?
if (app_enable_ssl) if (app_enable_ssl)
{ {
server.listen(443, server_cert.c_str(), server_key.c_str()); server.setCertificate(server_cert.c_str(), server_key.c_str());
//this creates a 2nd server listening on port 80 and redirects all requests HTTPS //this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer *redirectServer = new PsychicHttpServer(); PsychicHttpServer *redirectServer = new PsychicHttpServer();
redirectServer->config.ctrl_port = 20424; // just a random port different from the default one redirectServer->config.ctrl_port = 20424; // just a random port different from the default one
redirectServer->listen(80); redirectServer->onNotFound([](PsychicRequest *request, PsychicResponse *response) {
redirectServer->onNotFound([](PsychicRequest *request) {
String url = "https://" + request->host() + request->url(); String url = "https://" + request->host() + request->url();
return request->redirect(url.c_str()); return response->redirect(url.c_str());
}); });
} }
else
server.listen(80);
#else
server.listen(80);
#endif #endif
//serve static files from LittleFS/www on / only to clients on same wifi network //serve static files from LittleFS/www on / only to clients on same wifi network
//this is where our /index.html file lives //this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); server.serveStatic("/", LittleFS, "/www/")->addFilter(ON_STA_FILTER);
//serve static files from LittleFS/www-ap on / only to clients on SoftAP //serve static files from LittleFS/www-ap on / only to clients on SoftAP
//this is where our /index.html file lives //this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER);
//serve static files from LittleFS/img on /img //serve static files from LittleFS/img on /img
//it's more efficient to serve everything from a single www directory, but this is also possible. //it's more efficient to serve everything from a single www directory, but this is also possible.
@@ -240,16 +235,16 @@ void setup()
//example callback everytime a connection is opened //example callback everytime a connection is opened
server.onOpen([](PsychicClient *client) { server.onOpen([](PsychicClient *client) {
Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
}); });
//example callback everytime a connection is closed //example callback everytime a connection is closed
server.onClose([](PsychicClient *client) { server.onClose([](PsychicClient *client) {
Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
}); });
//api - json message passed in as post body //api - json message passed in as post body
server.on("/api", HTTP_POST, [](PsychicRequest *request) server.on("/api", HTTP_POST, [](PsychicRequest *request, PsychicResponse *response)
{ {
//load our JSON request //load our JSON request
StaticJsonDocument<1024> json; StaticJsonDocument<1024> json;
@@ -272,18 +267,18 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str());
}); });
//api - parameters passed in via query eg. /api/endpoint?foo=bar //api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/ip", HTTP_GET, [](PsychicRequest *request) server.on("/ip", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
String output = "Your IP is: " + request->client()->remoteIP().toString(); String output = "Your IP is: " + request->client()->remoteIP().toString();
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//api - parameters passed in via query eg. /api/endpoint?foo=bar //api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/api", HTTP_GET, [](PsychicRequest *request) server.on("/api", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
//create a response object //create a response object
StaticJsonDocument<128> output; StaticJsonDocument<128> output;
@@ -301,65 +296,64 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str());
}); });
//how to redirect a request //how to redirect a request
server.on("/redirect", HTTP_GET, [](PsychicRequest *request) server.on("/redirect", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
return request->redirect("/alien.png"); return response->redirect("/alien.png");
}); });
//how to do basic auth //how to do basic auth
server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
if (!request->authenticate(app_user, app_pass)) if (!request->authenticate(app_user, app_pass))
return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in."); return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in.");
return request->reply("Auth Basic Success!"); return response->send("Auth Basic Success!");
}); });
//how to do digest auth //how to do digest auth
server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
if (!request->authenticate(app_user, app_pass)) if (!request->authenticate(app_user, app_pass))
return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in."); return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in.");
return request->reply("Auth Digest Success!"); return response->send("Auth Digest Success!");
}); });
//example of getting / setting cookies //example of getting / setting cookies
server.on("/cookies", HTTP_GET, [](PsychicRequest *request) server.on("/cookies", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response)
{ {
PsychicResponse response(request);
int counter = 0; int counter = 0;
if (request->hasCookie("counter")) char cookie[14];
size_t size = 14;
if (request->getCookie("counter", cookie, &size) == ESP_OK)
{ {
counter = std::stoi(request->getCookie("counter").c_str()); // value is null-terminated.
counter = std::stoi(cookie);
counter++; counter++;
} }
sprintf(cookie, "%d", counter);
char cookie[10]; response->setCookie("counter", cookie);
sprintf(cookie, "%i", counter); response->setContent(cookie);
return response->send();
response.setCookie("counter", cookie);
response.setContent(cookie);
return response.send();
}); });
//example of getting POST variables //example of getting POST variables
server.on("/post", HTTP_POST, [](PsychicRequest *request) server.on("/post", HTTP_POST, [](PsychicRequest *request, PsychicResponse *response)
{ {
String output; String output;
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//you can set up a custom 404 handler. //you can set up a custom 404 handler.
server.onNotFound([](PsychicRequest *request) server.onNotFound([](PsychicRequest *request, PsychicResponse *response)
{ {
return request->reply(404, "text/html", "Custom 404 Handler"); return response->send(404, "text/html", "Custom 404 Handler");
}); });
//handle a very basic upload as post body //handle a very basic upload as post body
@@ -393,12 +387,12 @@ void setup()
}); });
//gets called after upload has been handled //gets called after upload has been handled
uploadHandler->onRequest([](PsychicRequest *request) uploadHandler->onRequest([](PsychicRequest *request, PsychicResponse *response)
{ {
String url = "/" + request->getFilename(); String url = "/" + request->getFilename();
String output = "<a href=\"" + url + "\">" + url + "</a>"; String output = "<a href=\"" + url + "\">" + url + "</a>";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//wildcard basic file upload - POST to /upload/filename.ext //wildcard basic file upload - POST to /upload/filename.ext
@@ -435,7 +429,7 @@ void setup()
}); });
//gets called after upload has been handled //gets called after upload has been handled
multipartHandler->onRequest([](PsychicRequest *request) multipartHandler->onRequest([](PsychicRequest *request, PsychicResponse *response)
{ {
PsychicWebParameter *file = request->getParam("file_upload"); PsychicWebParameter *file = request->getParam("file_upload");
@@ -447,7 +441,7 @@ void setup()
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//wildcard basic file upload - POST to /upload/filename.ext //wildcard basic file upload - POST to /upload/filename.ext
@@ -455,7 +449,7 @@ void setup()
//a websocket echo server //a websocket echo server
websocketHandler.onOpen([](PsychicWebSocketClient *client) { websocketHandler.onOpen([](PsychicWebSocketClient *client) {
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
client->sendMessage("Hello!"); client->sendMessage("Hello!");
}); });
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
@@ -463,17 +457,17 @@ void setup()
return request->reply(frame); return request->reply(frame);
}); });
websocketHandler.onClose([](PsychicWebSocketClient *client) { websocketHandler.onClose([](PsychicWebSocketClient *client) {
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
}); });
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
//EventSource server //EventSource server
eventSource.onOpen([](PsychicEventSourceClient *client) { eventSource.onOpen([](PsychicEventSourceClient *client) {
Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
client->send("Hello user!", NULL, millis(), 1000); client->send("Hello user!", NULL, millis(), 1000);
}); });
eventSource.onClose([](PsychicEventSourceClient *client) { eventSource.onClose([](PsychicEventSourceClient *client) {
Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString()); Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
}); });
server.on("/events", &eventSource); server.on("/events", &eventSource);
} }
@@ -486,10 +480,10 @@ void loop()
{ {
if (millis() - lastUpdate > 2000) if (millis() - lastUpdate > 2000)
{ {
sprintf(output, "Millis: %d\n", millis()); sprintf(output, "Millis: %lu\n", millis());
websocketHandler.sendAll(output); websocketHandler.sendAll(output);
sprintf(output, "%d", millis()); sprintf(output, "%lu", millis());
eventSource.send(output, "millis", millis(), 0); eventSource.send(output, "millis", millis(), 0);
lastUpdate = millis(); lastUpdate = millis();

View File

@@ -28,7 +28,7 @@ public:
esp_err_t handleRequest(PsychicRequest *request) { esp_err_t handleRequest(PsychicRequest *request) {
//PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html" //PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html"
//return response.send(); // uncomment : return captive portal page //return response.send(); // uncomment : return captive portal page
return request->reply(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page return response->send(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page
} }
}; };
CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal

View File

@@ -14,7 +14,7 @@
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <PsychicHttp.h> #include <PsychicHttp.h>
char* TAG = "CAPTPORT"; #define TAG "CAPTPORT"
// captiveportal // captiveportal
// credits https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino // credits https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino
@@ -29,10 +29,10 @@ public:
// ... if needed some tests ... return(false); // ... if needed some tests ... return(false);
return true; // activate captive portal return true; // activate captive portal
} }
esp_err_t handleRequest(PsychicRequest *request) { esp_err_t handleRequest(PsychicRequest *request, PsychicResponse *response) {
//PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html" //PsychicFileResponse response(request, LittleFS, "/captiveportal.html"); // uncomment : for captive portal page, if any, eg "captiveportal.html"
//return response.send(); // uncomment : return captive portal page //return response.send(); // uncomment : return captive portal page
return request->reply(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page return response->send(200,"text/html","Welcome to captive portal !"); // simple text, comment if captive portal page
} }
}; };
CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal CaptiveRequestHandler *captivehandler=NULL; // handler for captive portal
@@ -128,7 +128,6 @@ void setup() {
//setup server config stuff here //setup server config stuff here
server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls)
server.listen(80);
DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); DefaultHeaders::Instance().addHeader("Server", "PsychicHttp");

View File

@@ -8,7 +8,7 @@
CONDITIONS OF ANY KIND, either express or implied. CONDITIONS OF ANY KIND, either express or implied.
*/ */
char *TAG = "OTA"; // ESP_LOG tag #define TAG "OTA" // ESP_LOG tag
// PsychicHttp // PsychicHttp
#include <Arduino.h> #include <Arduino.h>
@@ -100,7 +100,6 @@ void setup()
//setup server config stuff here //setup server config stuff here
server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls)
server.listen(80);
DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); DefaultHeaders::Instance().addHeader("Server", "PsychicHttp");
@@ -109,8 +108,8 @@ void setup()
//you can set up a custom 404 handler. //you can set up a custom 404 handler.
// curl -i http://psychic.local/404 // curl -i http://psychic.local/404
server.onNotFound([](PsychicRequest *request) { server.onNotFound([](PsychicRequest *request, PsychicResponse *response) {
return request->reply(404, "text/html", "Custom 404 Handler"); return response->send(404, "text/html", "Custom 404 Handler");
}); });
// OTA // OTA
@@ -177,34 +176,34 @@ void setup()
} }
}); // end onUpload }); // end onUpload
updateHandler->onRequest([](PsychicRequest *request) { // triggered when update is completed (either OK or KO) and returns request's response (important) updateHandler->onRequest([](PsychicRequest *request, PsychicResponse *response) { // triggered when update is completed (either OK or KO) and returns request's response (important)
String result; // request result String result; // request result
// code below is executed when update is finished // code below is executed when update is finished
if (!Update.hasError()) { // update is OK if (!Update.hasError()) { // update is OK
ESP_LOGI(TAG,"Update code or data OK Update.errorString() %s", Update.errorString()); ESP_LOGI(TAG,"Update code or data OK Update.errorString() %s", Update.errorString());
result = "<b style='color:green'>Update done for file.</b>"; result = "<b style='color:green'>Update done for file.</b>";
return request->reply(200,"text/html",result.c_str()); return response->send(200,"text/html",result.c_str());
// ESP.restart(); // restart ESP if needed // ESP.restart(); // restart ESP if needed
} // end update is OK } // end update is OK
else { // update is KO, send request with pretty print error else { // update is KO, send request with pretty print error
result = " Update.errorString() " + String(Update.errorString()); result = " Update.errorString() " + String(Update.errorString());
ESP_LOGE(TAG,"ERROR : error %s",result.c_str()); ESP_LOGE(TAG,"ERROR : error %s",result.c_str());
return request->reply(500, "text/html", result.c_str()); return response->send(500, "text/html", result.c_str());
} // end update is KO } // end update is KO
}); });
server.on("/update", HTTP_GET, [](PsychicRequest*request){ server.on("/update", HTTP_GET, [](PsychicRequest*request, PsychicResponse *res){
PsychicFileResponse response(request, LittleFS, "/update.html"); PsychicFileResponse response(res, LittleFS, "/update.html");
return response.send(); return response.send();
}); });
server.on("/update", HTTP_POST, updateHandler); server.on("/update", HTTP_POST, updateHandler);
server.on("/restart", HTTP_POST, [](PsychicRequest *request) { server.on("/restart", HTTP_POST, [](PsychicRequest *request, PsychicResponse *response) {
String output = "<b style='color:green'>Restarting ...</b>"; String output = "<b style='color:green'>Restarting ...</b>";
ESP_LOGI(TAG,"%s",output.c_str()); ESP_LOGI(TAG,"%s",output.c_str());
esprestart=true; esprestart=true;
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
} // end onRequest } // end onRequest

View File

@@ -9,66 +9,69 @@
*/ */
/********************************************************************************************** /**********************************************************************************************
* Note: this demo relies on the following libraries (Install via Library Manager) * Note: this demo relies on the following libraries (Install via Library Manager)
* ArduinoJson UrlEncode * ArduinoJson UrlEncode
**********************************************************************************************/ **********************************************************************************************/
/********************************************************************************************** /**********************************************************************************************
* Note: this demo relies on various files to be uploaded on the LittleFS partition * Note: this demo relies on various files to be uploaded on the LittleFS partition
* Follow instructions here: https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/ * Follow instructions here: https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/
**********************************************************************************************/ **********************************************************************************************/
#include "secret.h"
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h>
#include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include "secret.h" #include <LittleFS.h>
#include <PsychicHttp.h> #include <PsychicHttp.h>
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE //set this to y in menuconfig to enable SSL #include <WiFi.h>
#include <PsychicHttpsServer.h> #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE // set this to y in menuconfig to enable SSL
#include <PsychicHttpsServer.h>
#endif #endif
#ifndef WIFI_SSID #ifndef WIFI_SSID
#error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there."
#endif #endif
//Enter your WIFI credentials in secret.h // Enter your WIFI credentials in secret.h
const char *ssid = WIFI_SSID; const char* ssid = WIFI_SSID;
const char *password = WIFI_PASS; const char* password = WIFI_PASS;
// Set your SoftAP credentials // Set your SoftAP credentials
const char *softap_ssid = "PsychicHttp"; const char* softap_ssid = "PsychicHttp";
const char *softap_password = ""; const char* softap_password = "";
IPAddress softap_ip(10, 0, 0, 1); IPAddress softap_ip(10, 0, 0, 1);
//credentials for the /auth-basic and /auth-digest examples // credentials for the /auth-basic and /auth-digest examples
const char *app_user = "admin"; const char* app_user = "admin";
const char *app_pass = "admin"; const char* app_pass = "admin";
const char *app_name = "Your App"; const char* app_name = "Your App";
//hostname for mdns (psychic.local) AuthenticationMiddleware basicAuth;
const char *local_hostname = "psychic"; AuthenticationMiddleware digestAuth;
//#define CONFIG_ESP_HTTPS_SERVER_ENABLE to enable ssl // hostname for mdns (psychic.local)
const char* local_hostname = "psychic";
// #define CONFIG_ESP_HTTPS_SERVER_ENABLE to enable ssl
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
bool app_enable_ssl = true; bool app_enable_ssl = true;
String server_cert; String server_cert;
String server_key; String server_key;
#endif #endif
//our main server object // our main server object
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
PsychicHttpsServer server; PsychicHttpsServer server;
#else #else
PsychicHttpServer server; PsychicHttpServer server;
#endif #endif
PsychicWebSocketHandler websocketHandler; PsychicWebSocketHandler websocketHandler;
PsychicEventSource eventSource; PsychicEventSource eventSource;
bool connectToWifi() bool connectToWifi()
{ {
//dual client and AP mode // dual client and AP mode
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
// Configure SoftAP // Configure SoftAP
@@ -92,10 +95,8 @@ bool connectToWifi()
int numberOfTries = 20; int numberOfTries = 20;
// Wait for the WiFi event // Wait for the WiFi event
while (true) while (true) {
{ switch (WiFi.status()) {
switch (WiFi.status())
{
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
Serial.println("[WiFi] SSID not found"); Serial.println("[WiFi] SSID not found");
break; break;
@@ -125,15 +126,12 @@ bool connectToWifi()
} }
delay(tryDelay); delay(tryDelay);
if (numberOfTries <= 0) if (numberOfTries <= 0) {
{
Serial.print("[WiFi] Failed to connect to WiFi!"); Serial.print("[WiFi] Failed to connect to WiFi!");
// Use disconnect function to force stop trying to connect // Use disconnect function to force stop trying to connect
WiFi.disconnect(); WiFi.disconnect();
return false; return false;
} } else {
else
{
numberOfTries--; numberOfTries--;
} }
} }
@@ -148,111 +146,102 @@ void setup()
// We start by connecting to a WiFi network // We start by connecting to a WiFi network
// To debug, please enable Core Debug Level to Verbose // To debug, please enable Core Debug Level to Verbose
if (connectToWifi()) if (connectToWifi()) {
{ // set up our esp32 to listen on the local_hostname.local domain
//set up our esp32 to listen on the local_hostname.local domain
if (!MDNS.begin(local_hostname)) { if (!MDNS.begin(local_hostname)) {
Serial.println("Error starting mDNS"); Serial.println("Error starting mDNS");
return; return;
} }
MDNS.addService("http", "tcp", 80); MDNS.addService("http", "tcp", 80);
if(!LittleFS.begin()) if (!LittleFS.begin()) {
{
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
//look up our keys? // look up our keys?
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
if (app_enable_ssl) if (app_enable_ssl) {
{ File fp = LittleFS.open("/server.crt");
File fp = LittleFS.open("/server.crt"); if (fp) {
if (fp) server_cert = fp.readString();
{
server_cert = fp.readString();
// Serial.println("Server Cert:"); // Serial.println("Server Cert:");
// Serial.println(server_cert); // Serial.println(server_cert);
} } else {
else Serial.println("server.pem not found, SSL not available");
{ app_enable_ssl = false;
Serial.println("server.pem not found, SSL not available");
app_enable_ssl = false;
}
fp.close();
File fp2 = LittleFS.open("/server.key");
if (fp2)
{
server_key = fp2.readString();
// Serial.println("Server Key:");
// Serial.println(server_key);
}
else
{
Serial.println("server.key not found, SSL not available");
app_enable_ssl = false;
}
fp2.close();
} }
#endif fp.close();
//setup server config stuff here File fp2 = LittleFS.open("/server.key");
server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) if (fp2) {
server_key = fp2.readString();
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE // Serial.println("Server Key:");
server.ssl_config.httpd.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) // Serial.println(server_key);
} else {
Serial.println("server.key not found, SSL not available");
app_enable_ssl = false;
}
fp2.close();
}
#endif
//do we want secure or not? // setup server config stuff here
if (app_enable_ssl) server.config.max_uri_handlers = 20; // maximum number of uri handlers (.on() calls)
{
server.listen(443, server_cert.c_str(), server_key.c_str()); #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
server.ssl_config.httpd.max_uri_handlers = 20; // maximum number of uri handlers (.on() calls)
//this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer *redirectServer = new PsychicHttpServer(); // do we want secure or not?
redirectServer->config.ctrl_port = 20424; // just a random port different from the default one if (app_enable_ssl) {
redirectServer->listen(80); server.setCertificate(server_cert.c_str(), server_key.c_str());
redirectServer->onNotFound([](PsychicRequest *request) {
// this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer* redirectServer = new PsychicHttpServer();
redirectServer->config.ctrl_port = 20424; // just a random port different from the default one
redirectServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) {
String url = "https://" + request->host() + request->url(); String url = "https://" + request->host() + request->url();
return request->redirect(url.c_str()); return response->redirect(url.c_str()); });
}); }
} #endif
else
server.listen(80);
#else
server.listen(80);
#endif
//serve static files from LittleFS/www on / only to clients on same wifi network basicAuth.setUsername(app_user);
//this is where our /index.html file lives basicAuth.setPassword(app_pass);
server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER); basicAuth.setRealm(app_name);
basicAuth.setAuthMethod(HTTPAuthMethod::BASIC_AUTH);
basicAuth.setAuthFailureMessage("You must log in.");
//serve static files from LittleFS/www-ap on / only to clients on SoftAP digestAuth.setUsername(app_user);
//this is where our /index.html file lives digestAuth.setPassword(app_pass);
server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); digestAuth.setRealm(app_name);
digestAuth.setAuthMethod(HTTPAuthMethod::DIGEST_AUTH);
digestAuth.setAuthFailureMessage("You must log in.");
//serve static files from LittleFS/img on /img // serve static files from LittleFS/www on / only to clients on same wifi network
//it's more efficient to serve everything from a single www directory, but this is also possible. // this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www/")->addFilter(ON_STA_FILTER);
// serve static files from LittleFS/www-ap on / only to clients on SoftAP
// this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER);
// serve static files from LittleFS/img on /img
// it's more efficient to serve everything from a single www directory, but this is also possible.
server.serveStatic("/img", LittleFS, "/img/"); server.serveStatic("/img", LittleFS, "/img/");
//you can also serve single files // you can also serve single files
server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); server.serveStatic("/myfile.txt", LittleFS, "/custom.txt");
//example callback everytime a connection is opened // example callback everytime a connection is opened
server.onOpen([](PsychicClient *client) { server.onOpen([](PsychicClient* client) { Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); });
Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
});
//example callback everytime a connection is closed // example callback everytime a connection is closed
server.onClose([](PsychicClient *client) { server.onClose([](PsychicClient* client) { Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); });
Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
});
//api - json message passed in as post body // api - json message passed in as post body
server.on("/api", HTTP_POST, [](PsychicRequest *request) server.on("/api", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) {
{
//load our JSON request //load our JSON request
JsonDocument json; JsonDocument json;
String body = request->body(); String body = request->body();
@@ -284,19 +273,15 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str()); });
});
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/ip", HTTP_GET, [](PsychicRequest *request) server.on("/ip", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{
String output = "Your IP is: " + request->client()->remoteIP().toString(); String output = "Your IP is: " + request->client()->remoteIP().toString();
return request->reply(output.c_str()); return response->send(output.c_str()); });
});
//api - parameters passed in via query eg. /api/endpoint?foo=bar // api - parameters passed in via query eg. /api/endpoint?foo=bar
server.on("/api", HTTP_GET, [](PsychicRequest *request) server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{
//create a response object //create a response object
JsonDocument output; JsonDocument output;
output["msg"] = "status"; output["msg"] = "status";
@@ -313,70 +298,48 @@ void setup()
//serialize and return //serialize and return
String jsonBuffer; String jsonBuffer;
serializeJson(output, jsonBuffer); serializeJson(output, jsonBuffer);
return request->reply(200, "application/json", jsonBuffer.c_str()); return response->send(200, "application/json", jsonBuffer.c_str()); });
});
//how to redirect a request // how to redirect a request
server.on("/redirect", HTTP_GET, [](PsychicRequest *request) server.on("/redirect", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/alien.png"); });
{
return request->redirect("/alien.png");
});
//how to do basic auth // how to do basic auth
server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) server.on("/auth-basic", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Basic Success!"); })->addMiddleware(&basicAuth);
{
if (!request->authenticate(app_user, app_pass))
return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in.");
return request->reply("Auth Basic Success!");
});
//how to do digest auth // how to do digest auth
server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) server.on("/auth-digest", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Digest Success!"); })->addMiddleware(&digestAuth);
{
if (!request->authenticate(app_user, app_pass))
return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in.");
return request->reply("Auth Digest Success!");
});
//example of getting / setting cookies
server.on("/cookies", HTTP_GET, [](PsychicRequest *request)
{
PsychicResponse response(request);
// example of getting / setting cookies
server.on("/cookies", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
int counter = 0; int counter = 0;
if (request->hasCookie("counter")) char cookie[14];
size_t size = 14;
if (request->getCookie("counter", cookie, &size) == ESP_OK)
{ {
counter = std::stoi(request->getCookie("counter").c_str()); // value is null-terminated.
counter = std::stoi(cookie);
counter++; counter++;
} }
sprintf(cookie, "%d", counter);
char cookie[12]; response->setCookie("counter", cookie);
sprintf(cookie, "%i", counter); response->setContent(cookie);
return response->send(); });
response.setCookie("counter", cookie); // example of getting POST variables
response.setContent(cookie); server.on("/post", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) {
return response.send();
});
//example of getting POST variables
server.on("/post", HTTP_POST, [](PsychicRequest *request)
{
String output; String output;
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str()); });
});
//you can set up a custom 404 handler. // you can set up a custom 404 handler.
server.onNotFound([](PsychicRequest *request) server.onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "Custom 404 Handler"); });
{
return request->reply(404, "text/html", "Custom 404 Handler");
});
//handle a very basic upload as post body // handle a very basic upload as post body
PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); PsychicUploadHandler* uploadHandler = new PsychicUploadHandler();
uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { uploadHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) {
File file; File file;
String path = "/www/" + filename; String path = "/www/" + filename;
@@ -401,24 +364,21 @@ void setup()
return ESP_FAIL; return ESP_FAIL;
} }
return ESP_OK; return ESP_OK; });
});
//gets called after upload has been handled // gets called after upload has been handled
uploadHandler->onRequest([](PsychicRequest *request) uploadHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) {
{
String url = "/" + request->getFilename(); String url = "/" + request->getFilename();
String output = "<a href=\"" + url + "\">" + url + "</a>"; String output = "<a href=\"" + url + "\">" + url + "</a>";
return request->reply(output.c_str()); return response->send(output.c_str()); });
});
//wildcard basic file upload - POST to /upload/filename.ext // wildcard basic file upload - POST to /upload/filename.ext
server.on("/upload/*", HTTP_POST, uploadHandler); server.on("/upload/*", HTTP_POST, uploadHandler);
//a little bit more complicated multipart form // a little bit more complicated multipart form
PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); PsychicUploadHandler* multipartHandler = new PsychicUploadHandler();
multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { multipartHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) {
File file; File file;
String path = "/www/" + filename; String path = "/www/" + filename;
@@ -443,12 +403,10 @@ void setup()
return ESP_FAIL; return ESP_FAIL;
} }
return ESP_OK; return ESP_OK; });
});
//gets called after upload has been handled // gets called after upload has been handled
multipartHandler->onRequest([](PsychicRequest *request) multipartHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) {
{
PsychicWebParameter *file = request->getParam("file_upload"); PsychicWebParameter *file = request->getParam("file_upload");
String url = "/" + file->value(); String url = "/" + file->value();
@@ -459,34 +417,26 @@ void setup()
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str()); });
});
//wildcard basic file upload - POST to /upload/filename.ext // wildcard basic file upload - POST to /upload/filename.ext
server.on("/multipart", HTTP_POST, multipartHandler); server.on("/multipart", HTTP_POST, multipartHandler);
//a websocket echo server // a websocket echo server
websocketHandler.onOpen([](PsychicWebSocketClient *client) { websocketHandler.onOpen([](PsychicWebSocketClient* client) {
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
client->sendMessage("Hello!"); client->sendMessage("Hello!"); });
}); websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) {
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload);
return request->reply(frame); return request->reply(frame); });
}); websocketHandler.onClose([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); });
websocketHandler.onClose([](PsychicWebSocketClient *client) {
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
});
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
//EventSource server // EventSource server
eventSource.onOpen([](PsychicEventSourceClient *client) { eventSource.onOpen([](PsychicEventSourceClient* client) {
Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str());
client->send("Hello user!", NULL, millis(), 1000); client->send("Hello user!", NULL, millis(), 1000); });
}); eventSource.onClose([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); });
eventSource.onClose([](PsychicEventSourceClient *client) {
Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str());
});
server.on("/events", &eventSource); server.on("/events", &eventSource);
} }
} }
@@ -496,8 +446,7 @@ char output[60];
void loop() void loop()
{ {
if (millis() - lastUpdate > 2000) if (millis() - lastUpdate > 2000) {
{
sprintf(output, "Millis: %lu\n", millis()); sprintf(output, "Millis: %lu\n", millis());
websocketHandler.sendAll(output); websocketHandler.sendAll(output);

View File

@@ -1,39 +0,0 @@
.pioenvs
.clang_complete
.gcc-flags.json
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Visual Studio/VisualMicro stuff
Visual\ Micro
*.sdf
*.opensdf
*.suo
.pioenvs
.piolibdeps
.pio
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/settings.json
.vscode/.browse.c_cpp.db*
.vscode/ipch

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -1,74 +0,0 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[common]
lib_deps = ArduinoMongoose
monitor_speed = 115200
monitor_port = /dev/ttyUSB1
build_flags =
-DENABLE_DEBUG
# -DCS_ENABLE_STDIO
-DMG_ENABLE_HTTP_STREAMING_MULTIPART=1
build_flags_secure =
-DSIMPLE_SERVER_SECURE
-DMG_ENABLE_SSL=1
# -DMG_SSL_IF=MG_SSL_IF_OPENSSL
# -DKR_VERSION
-DMG_SSL_MBED_DUMMY_RANDOM=1
-DMG_SSL_IF=MG_SSL_IF_MBEDTLS
-DMG_SSL_IF_MBEDTLS_FREE_CERTS=1
-DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048
build_flags_auth =
-DADMIN_USER='"admin"'
-DADMIN_PASS='"admin"'
-DADMIN_REALM='"esp_ota_http_server"'
#[env:huzzah]
#platform = espressif8266
#board = huzzah
#framework = arduino
#monitor_speed = ${common.monitor_speed}
#monitor_port = ${common.monitor_port}
#lib_deps = ${common.lib_deps}
#build_flags = ${common.build_flags}
[env:esp-wrover-kit]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
[env:esp-wrover-kit-secure]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags} ${common.build_flags_secure}
[env:esp-wrover-kit-auth]
extends = env:esp-wrover-kit
build_flags = ${common.build_flags} ${common.build_flags_auth} -ggdb
#[env:linux_x86_64]
#platform = linux_x86_64
#framework = arduino
#board = generic
#lib_deps = ${common.lib_deps}
#build_flags = ${common.build_flags}
#build_flags = -DSERIAL_TO_CONSOLE

View File

@@ -1,229 +0,0 @@
//
// A simple server implementation showing how to:
// * serve static messages
// * read GET and POST parameters
// * handle missing pages / 404s
//
#include <Arduino.h>
#include <MongooseCore.h>
#include <MongooseHttpServer.h>
#include <Update.h>
#ifdef ESP32
#include <WiFi.h>
#define START_ESP_WIFI
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#define START_ESP_WIFI
#else
#error Platform not supported
#endif
MongooseHttpServer server;
const char *ssid = "wifi";
const char *password = "password";
const char *server_pem =
"-----BEGIN CERTIFICATE-----\r\n"
"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n"
"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n"
"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n"
"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n"
"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n"
"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n"
"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n"
"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n"
"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n"
"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n"
"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n"
"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n"
"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n"
"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n"
"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n"
"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n"
"gJ2sWjUlokrbHswSBLLbUJIF\r\n"
"-----END CERTIFICATE-----\r\n";
const char *server_key =
"-----BEGIN PRIVATE KEY-----\r\n"
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n"
"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n"
"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n"
"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n"
"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n"
"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n"
"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n"
"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n"
"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n"
"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n"
"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n"
"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n"
"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n"
"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n"
"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n"
"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n"
"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n"
"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n"
"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n"
"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n"
"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n"
"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n"
"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n"
"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n"
"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n"
"7G/qUI9iE5iUU7MkIcLIbTf/\r\n"
"-----END PRIVATE KEY-----\r\n";
const char* server_index =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
"$.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>";
#include <Arduino.h>
bool updateCompleted = false;
static void updateError(MongooseHttpServerRequest *request)
{
MongooseHttpServerResponseStream *resp = request->beginResponseStream();
resp->setCode(500);
resp->setContentType("text/plain");
resp->printf("Error: %d", Update.getError());
request->send(resp);
// Anoyingly this uses Stream rather than Print...
Update.printError(Serial);
}
void setup()
{
Serial.begin(115200);
#ifdef START_ESP_WIFI
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Hostname: ");
#ifdef ESP32
Serial.println(WiFi.getHostname());
#elif defined(ESP8266)
Serial.println(WiFi.hostname());
#endif
#endif
Mongoose.begin();
#ifdef SIMPLE_SERVER_SECURE
if(false == server.begin(443, server_pem, server_key)) {
Serial.print("Failed to start server");
return;
}
#else
server.begin(80);
#endif
server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) {
#if defined(ADMIN_USER) && defined(ADMIN_PASS) && defined(ADMIN_REALM)
if(false == request->authenticate(ADMIN_USER, ADMIN_PASS)) {
request->requestAuthentication(ADMIN_REALM);
return;
}
#endif
request->send(200, "text/html", server_index);
});
server.on("/update$", HTTP_POST)->
onRequest([](MongooseHttpServerRequest *request) {
#if defined(ADMIN_USER) && defined(ADMIN_PASS) && defined(ADMIN_REALM)
if(false == request->authenticate(ADMIN_USER, ADMIN_PASS)) {
request->requestAuthentication(ADMIN_REALM);
return;
}
#endif
updateCompleted = false;
})->
onUpload([](MongooseHttpServerRequest *request, int ev, MongooseString filename, uint64_t index, uint8_t *data, size_t len)
{
if(MG_EV_HTTP_PART_BEGIN == ev) {
Serial.printf("Update Start: %s\n", filename.c_str());
if (!Update.begin()) { //start with max available size
updateError(request);
}
}
if(!Update.hasError())
{
Serial.printf("Update Writing %llu\n", index);
if(Update.write(data, len) != len) {
updateError(request);
}
}
if(MG_EV_HTTP_PART_END == ev) {
Serial.println("Data finished");
if(Update.end(true)) {
Serial.printf("Update Success: %lluB\n", index+len);
request->send(200, "text/plain", "OK");
updateCompleted = true;
} else {
updateError(request);
}
}
return len;
})->
onClose([](MongooseHttpServerRequest *request)
{
if(updateCompleted) {
ESP.restart();
}
});
}
void loop()
{
Mongoose.poll(1000);
}

View File

@@ -1,11 +0,0 @@
This directory is intended for PIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html

View File

@@ -1,39 +0,0 @@
.pioenvs
.clang_complete
.gcc-flags.json
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Visual Studio/VisualMicro stuff
Visual\ Micro
*.sdf
*.opensdf
*.suo
.pioenvs
.piolibdeps
.pio
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/settings.json
.vscode/.browse.c_cpp.db*
.vscode/ipch

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -1,64 +0,0 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[common]
lib_deps = ArduinoMongoose
monitor_speed = 115200
monitor_port = /dev/ttyUSB1
build_flags =
-DENABLE_DEBUG
# -DCS_ENABLE_STDIO
build_flags_secure =
-DSIMPLE_SERVER_SECURE
-DMG_ENABLE_SSL=1
# -DMG_SSL_IF=MG_SSL_IF_OPENSSL
# -DKR_VERSION
-DMG_SSL_MBED_DUMMY_RANDOM=1
-DMG_SSL_IF=MG_SSL_IF_MBEDTLS
-DMG_SSL_IF_MBEDTLS_FREE_CERTS=1
-DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048
[env:huzzah]
platform = espressif8266
board = huzzah
framework = arduino
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
[env:esp-wrover-kit]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
[env:esp-wrover-kit-secure]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags} ${common.build_flags_secure}
#[env:linux_x86_64]
#platform = linux_x86_64
#framework = arduino
#board = generic
#lib_deps = ${common.lib_deps}
#build_flags = ${common.build_flags}
#build_flags = -DSERIAL_TO_CONSOLE

View File

@@ -1,211 +0,0 @@
//
// A simple server implementation showing how to:
// * serve static messages
// * read GET and POST parameters
// * handle missing pages / 404s
//
#include <Arduino.h>
#include <MongooseCore.h>
#include <MongooseHttpServer.h>
#ifdef ESP32
#include <WiFi.h>
#define START_ESP_WIFI
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#define START_ESP_WIFI
#else
#error Platform not supported
#endif
MongooseHttpServer server;
const char *ssid = "wifi";
const char *password = "password";
const char *PARAM_MESSAGE = "message";
const char *server_pem =
"-----BEGIN CERTIFICATE-----\r\n"
"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n"
"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n"
"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n"
"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n"
"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n"
"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n"
"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n"
"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n"
"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n"
"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n"
"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n"
"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n"
"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n"
"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n"
"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n"
"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n"
"gJ2sWjUlokrbHswSBLLbUJIF\r\n"
"-----END CERTIFICATE-----\r\n";
const char *server_key =
"-----BEGIN PRIVATE KEY-----\r\n"
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n"
"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n"
"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n"
"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n"
"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n"
"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n"
"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n"
"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n"
"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n"
"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n"
"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n"
"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n"
"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n"
"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n"
"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n"
"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n"
"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n"
"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n"
"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n"
"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n"
"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n"
"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n"
"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n"
"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n"
"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n"
"7G/qUI9iE5iUU7MkIcLIbTf/\r\n"
"-----END PRIVATE KEY-----\r\n";
static void notFound(MongooseHttpServerRequest *request);
#include <Arduino.h>
void setup()
{
Serial.begin(115200);
#ifdef START_ESP_WIFI
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Hostname: ");
#ifdef ESP32
Serial.println(WiFi.getHostname());
#elif defined(ESP8266)
Serial.println(WiFi.hostname());
#endif
#endif
Mongoose.begin();
#ifdef SIMPLE_SERVER_SECURE
if(false == server.begin(443, server_pem, server_key)) {
Serial.print("Failed to start server");
return;
}
#else
server.begin(80);
#endif
server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) {
request->send(200, "text/plain", "Hello world");
});
// Send a GET request to <IP>/get?message=<message>
server.on("/get$", HTTP_GET, [](MongooseHttpServerRequest *request) {
String message;
if (request->hasParam(PARAM_MESSAGE))
{
message = request->getParam(PARAM_MESSAGE);
}
else
{
message = "No message sent";
}
request->send(200, "text/plain", "Hello, GET: " + message);
});
// Send a POST request to <IP>/post with a form field message set to <message>
server.on("/post$", HTTP_POST, [](MongooseHttpServerRequest *request) {
String message;
if (request->hasParam(PARAM_MESSAGE))
{
message = request->getParam(PARAM_MESSAGE);
}
else
{
message = "No message sent";
}
request->send(200, "text/plain", "Hello, POST: " + message);
});
// Test the basic response class
server.on("/basic$", HTTP_GET, [](MongooseHttpServerRequest *request) {
MongooseHttpServerResponseBasic *resp = request->beginResponse();
resp->setCode(200);
resp->setContentType("text/html");
resp->addHeader("Cache-Control", "max-age=300");
resp->addHeader("X-hello", "world");
resp->setContent(
"<html>\n"
"<head>\n"
"<title>Basic Page</title>\n"
"</head>\n"
"<body>\n"
"<h1>Basic Page</h1>\n"
"<p>\n"
"This page has been sent using the MongooseHttpServerResponseBasic class\n"
"</p>\n"
"</body>\n"
"</html>\n");
request->send(resp);
});
// Test the stream response class
server.on("/stream$", HTTP_GET, [](MongooseHttpServerRequest *request) {
MongooseHttpServerResponseStream *resp = request->beginResponseStream();
resp->setCode(200);
resp->setContentType("text/html");
resp->addHeader("Cache-Control", "max-age=300");
resp->addHeader("X-hello", "world");
resp->println("<html>");
resp->println("<head>");
resp->println("<title>Stream Page</title>");
resp->println("</head>");
resp->println("<body>");
resp->println("<h1>Stream Page</h1>");
resp->println("<p>");
resp->println("This page has been sent using the MongooseHttpServerResponseStream class");
resp->println("</p>");
resp->println("<p>");
resp->printf("micros = %lu<br/>", micros());
resp->printf("free = %u<br/>", ESP.getFreeHeap());
resp->println("</p>");
resp->println("</body>");
resp->println("</html>");
request->send(resp);
});
server.onNotFound(notFound);
}
void loop()
{
Mongoose.poll(1000);
Serial.printf("Free memory %u\n", ESP.getFreeHeap());
}
static void notFound(MongooseHttpServerRequest *request)
{
request->send(404, "text/plain", "Not found");
}

View File

@@ -1,11 +0,0 @@
This directory is intended for PIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html

View File

@@ -1,35 +0,0 @@
# Name: REST Client
# Id: humao.rest-client
# Description: REST Client for Visual Studio Code
# Version: 0.21.3
# Publisher: Huachao Mao
# VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=humao.rest-client
@baseUrl = http://172.16.0.87
###
GET {{baseUrl}}/ HTTP/1.1
###
GET {{baseUrl}}/get?message=Hello+World HTTP/1.1
###
POST {{baseUrl}}/post HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
message=Hello+World
###
GET {{baseUrl}}/someRandomFile HTTP/1.1
###
GET {{baseUrl}}/basic HTTP/1.1
###
GET {{baseUrl}}/stream HTTP/1.1

View File

@@ -1,39 +0,0 @@
.pioenvs
.clang_complete
.gcc-flags.json
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Visual Studio/VisualMicro stuff
Visual\ Micro
*.sdf
*.opensdf
*.suo
.pioenvs
.piolibdeps
.pio
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/settings.json
.vscode/.browse.c_cpp.db*
.vscode/ipch

View File

@@ -1,67 +0,0 @@
# Continuous Integration (CI) is the practice, in software
# engineering, of merging all developer working copies with a shared mainline
# several times a day < https://docs.platformio.org/page/ci/index.html >
#
# Documentation:
#
# * Travis CI Embedded Builds with PlatformIO
# < https://docs.travis-ci.com/user/integration/platformio/ >
#
# * PlatformIO integration with Travis CI
# < https://docs.platformio.org/page/ci/travis.html >
#
# * User Guide for `platformio ci` command
# < https://docs.platformio.org/page/userguide/cmd_ci.html >
#
#
# Please choose one of the following templates (proposed below) and uncomment
# it (remove "# " before each line) or use own configuration according to the
# Travis CI documentation (see above).
#
#
# Template #1: General project. Test it using existing `platformio.ini`.
#
# language: python
# python:
# - "2.7"
#
# sudo: false
# cache:
# directories:
# - "~/.platformio"
#
# install:
# - pip install -U platformio
# - platformio update
#
# script:
# - platformio run
#
# Template #2: The project is intended to be used as a library with examples.
#
# language: python
# python:
# - "2.7"
#
# sudo: false
# cache:
# directories:
# - "~/.platformio"
#
# env:
# - PLATFORMIO_CI_SRC=path/to/test/file.c
# - PLATFORMIO_CI_SRC=examples/file.ino
# - PLATFORMIO_CI_SRC=path/to/test/directory
#
# install:
# - pip install -U platformio
# - platformio update
#
# script:
# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -1,40 +0,0 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[common]
lib_deps = ArduinoMongoose
monitor_speed = 115200
build_flags =
[espressif8266]
build_flags = -DMG_ESP8266
[espressif32]
build_flags =
[env:huzzah]
platform = espressif8266
framework = arduino
board = huzzah
monitor_speed = ${common.monitor_speed}
lib_deps = ${common.lib_deps}
build_flags =
${espressif8266.build_flags}
${common.build_flags}
[env:espwroverkit]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
lib_deps = ${common.lib_deps}
build_flags =
${espressif32.build_flags}
${common.build_flags}

View File

@@ -1,97 +0,0 @@
// Copyright (c) 2015 Cesanta Software Limited
// All rights reserved
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#else
#error Platform not supported
#endif
#include "mongoose.h"
const char* ssid = "my-ssid";
const char* password = "my-password";
static const char *s_http_port = "80";
//static struct mg_serve_http_opts s_http_server_opts;
static void ev_handler(struct mg_connection *nc, int ev, void *p, void *d) {
static const char *reply_fmt =
"HTTP/1.0 200 OK\r\n"
"Connection: close\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"Hello %s\n";
switch (ev) {
case MG_EV_ACCEPT: {
char addr[32];
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr),
MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
Serial.printf("Connection %p from %s\n", nc, addr);
break;
}
case MG_EV_HTTP_REQUEST: {
char addr[32];
struct http_message *hm = (struct http_message *) p;
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr),
MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
Serial.printf("HTTP request from %s: %.*s %.*s\n", addr, (int) hm->method.len,
hm->method.p, (int) hm->uri.len, hm->uri.p);
mg_printf(nc, reply_fmt, addr);
nc->flags |= MG_F_SEND_AND_CLOSE;
break;
}
case MG_EV_CLOSE: {
Serial.printf("Connection %p closed\n", nc);
break;
}
}
}
struct mg_mgr mgr;
struct mg_connection *nc;
void setup()
{
Serial.begin(115200);
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
mg_mgr_init(&mgr, NULL);
Serial.printf("Starting web server on port %s\n", s_http_port);
nc = mg_bind(&mgr, s_http_port, ev_handler, NULL);
if (nc == NULL) {
Serial.printf("Failed to create listener\n");
return;
}
// Set up HTTP server parameters
mg_set_protocol_http_websocket(nc);
// s_http_server_opts.document_root = "."; // Serve current directory
// s_http_server_opts.enable_directory_listing = "yes";
}
static uint32_t count = 0;
void loop()
{
mg_mgr_poll(&mgr, 1000);
//Serial.println(count++);
}

View File

@@ -1,11 +0,0 @@
This directory is intended for PIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html

View File

@@ -1,39 +0,0 @@
.pioenvs
.clang_complete
.gcc-flags.json
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Visual Studio/VisualMicro stuff
Visual\ Micro
*.sdf
*.opensdf
*.suo
.pioenvs
.piolibdeps
.pio
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/settings.json
.vscode/.browse.c_cpp.db*
.vscode/ipch

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -1,64 +0,0 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[common]
lib_deps = ArduinoMongoose
monitor_speed = 115200
monitor_port = /dev/ttyUSB2
build_flags =
-DENABLE_DEBUG
# -DCS_ENABLE_STDIO
build_flags_secure =
-DSIMPLE_SERVER_SECURE
-DMG_ENABLE_SSL=1
# -DMG_SSL_IF=MG_SSL_IF_OPENSSL
# -DKR_VERSION
-DMG_SSL_MBED_DUMMY_RANDOM=1
-DMG_SSL_IF=MG_SSL_IF_MBEDTLS
-DMG_SSL_IF_MBEDTLS_FREE_CERTS=1
-DMG_SSL_IF_MBEDTLS_MAX_FRAG_LEN=2048
[env:huzzah]
platform = espressif8266
board = huzzah
framework = arduino
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
[env:esp-wrover-kit]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
[env:esp-wrover-kit-secure]
platform = espressif32
framework = arduino
board = esp-wrover-kit
monitor_speed = ${common.monitor_speed}
monitor_port = ${common.monitor_port}
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags} ${common.build_flags_secure}
#[env:linux_x86_64]
#platform = linux_x86_64
#framework = arduino
#board = generic
#lib_deps = ${common.lib_deps}
#build_flags = ${common.build_flags}
#build_flags = -DSERIAL_TO_CONSOLE

View File

@@ -1,235 +0,0 @@
//
// A simple server implementation showing how to:
// * serve static messages
// * read GET and POST parameters
// * handle missing pages / 404s
//
#include <Arduino.h>
#include <MongooseCore.h>
#include <MongooseHttpServer.h>
#ifdef ESP32
#include <WiFi.h>
#define START_ESP_WIFI
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#define START_ESP_WIFI
#else
#error Platform not supported
#endif
MongooseHttpServer server;
const char *ssid = "wifi";
const char *password = "password";
const char *server_pem =
"-----BEGIN CERTIFICATE-----\r\n"
"MIIDDjCCAfagAwIBAgIBBDANBgkqhkiG9w0BAQsFADA/MRkwFwYDVQQDDBB0ZXN0\r\n"
"LmNlc2FudGEuY29tMRAwDgYDVQQKDAdDZXNhbnRhMRAwDgYDVQQLDAd0ZXN0aW5n\r\n"
"MB4XDTE2MTExMzEzMTgwMVoXDTI2MDgxMzEzMTgwMVowFDESMBAGA1UEAwwJbG9j\r\n"
"YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAro8CW1X0xaGm\r\n"
"GkDaMxKbXWA5Lw+seA61tioGrSIQzuqLYeJoFnwVgF0jB5PTj+3EiGMBcA/mh73V\r\n"
"AthTFmJBxj+agIp7/cvUBpgfLClmSYL2fZi6Fodz+f9mcry3XRw7O6vlamtWfTX8\r\n"
"TAmMSR6PXVBHLgjs5pDOFFmrNAsM5sLYU1/1MFvE2Z9InTI5G437IE1WchRSbpYd\r\n"
"HchC39XzpDGoInZB1a3OhcHm+xUtLpMJ0G0oE5VFEynZreZoEIY4JxspQ7LPsay9\r\n"
"fx3Tlk09gEMQgVCeCNiQwUxZdtLau2x61LNcdZCKN7FbFLJszv1U2uguELsTmi7E\r\n"
"6pHrTziosQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIDqDATBgNVHSUE\r\n"
"DDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB\r\n"
"AQBUw0hbTcT6crzODO4QAXU7z4Xxn0LkxbXEsoThG1QCVgMc4Bhpx8gyz5CLyHYz\r\n"
"AiJOBFEeV0XEqoGTNMMFelR3Q5Tg9y1TYO3qwwAWxe6/brVzpts6NiG1uEMBnBFg\r\n"
"oN1x3I9x4NpOxU5MU1dlIxvKs5HQCoNJ8D0SqOX9BV/pZqwEgiCbuWDWQAlxkFpn\r\n"
"iLonlkVI5hTuybCSBsa9FEI9M6JJn9LZmlH90FYHeS4t6P8eOJCeekHL0jUG4Iae\r\n"
"DMP12h8Sd0yxIKmmZ+Q/p/D/BkuHf5Idv3hgyLkZ4mNznjK49wHaYM+BgBoL3Zeg\r\n"
"gJ2sWjUlokrbHswSBLLbUJIF\r\n"
"-----END CERTIFICATE-----\r\n";
const char *server_key =
"-----BEGIN PRIVATE KEY-----\r\n"
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCujwJbVfTFoaYa\r\n"
"QNozEptdYDkvD6x4DrW2KgatIhDO6oth4mgWfBWAXSMHk9OP7cSIYwFwD+aHvdUC\r\n"
"2FMWYkHGP5qAinv9y9QGmB8sKWZJgvZ9mLoWh3P5/2ZyvLddHDs7q+Vqa1Z9NfxM\r\n"
"CYxJHo9dUEcuCOzmkM4UWas0CwzmwthTX/UwW8TZn0idMjkbjfsgTVZyFFJulh0d\r\n"
"yELf1fOkMagidkHVrc6Fweb7FS0ukwnQbSgTlUUTKdmt5mgQhjgnGylDss+xrL1/\r\n"
"HdOWTT2AQxCBUJ4I2JDBTFl20tq7bHrUs1x1kIo3sVsUsmzO/VTa6C4QuxOaLsTq\r\n"
"ketPOKixAgMBAAECggEAI+uNwpnHirue4Jwjyoqzqd1ZJxQEm5f7UIcJZKsz5kBh\r\n"
"ej0KykWybv27bZ2/1UhKPv6QlyzOdXRc1v8I6fxCKLeB5Z2Zsjo1YT4AfCfwwoPO\r\n"
"kT3SXTx2YyVpQYcP/HsIvVi8FtALtixbxJHaall9iugwHYr8pN17arihAE6d0wZC\r\n"
"JXtXRjUWwjKzXP8FoH4KhyadhHbDwIbbJe3cyLfdvp54Gr0YHha0JcOxYgDYNya4\r\n"
"OKxlCluI+hPF31iNzOmFLQVrdYynyPcR6vY5XOiANKE2iNbqCzRb54CvW9WMqObX\r\n"
"RD9t3DMOxGsbVNIwyzZndWy13HoQMGnrHfnGak9ueQKBgQDiVtOqYfLnUnTxvJ/b\r\n"
"qlQZr2ZmsYPZztxlP+DSqZGPD+WtGSo9+rozWfzjTv3KGIDLvf+GFVmjVHwlLQfd\r\n"
"u7eTemWHFc4HK68wruzPO/FdyVpQ4w9v3Usg+ll4a/PDEId0fDMjAr6kk4LC6t8y\r\n"
"9fJR0HjOz57jVnlrDt3v50G8BwKBgQDFbw+jRiUxXnBbDyXZLi+I4iGBGdC+CbaJ\r\n"
"CmsM6/TsOFc+GRsPwQF1gCGqdaURw76noIVKZJOSc8I+yiwU6izyh/xaju5JiWQd\r\n"
"kwbU1j4DE6GnxmT3ARmB7VvCxjaEZEAtICWs1QTKRz7PcTV8yr7Ng1A3VIy+NSpo\r\n"
"LFMMmk83hwKBgQDVCEwpLg/mUeHoNVVw95w4oLKNLb+gHeerFLiTDy8FrDzM88ai\r\n"
"l37yHly7xflxYia3nZkHpsi7xiUjCINC3BApKyasQoWskh1OgRY653yCfaYYQ96f\r\n"
"t3WjEH9trI2+p6wWo1+uMEMnu/9zXoW9/WeaQdGzNg+igh29+jxCNTPVuQKBgGV4\r\n"
"CN9vI5pV4QTLqjYOSJvfLDz/mYqxz0BrPE1tz3jAFAZ0PLZCCY/sBGFpCScyJQBd\r\n"
"vWNYgYeZOtGuci1llSgov4eDQfBFTlDsyWwFl+VY55IkoqtXw1ZFOQ3HdSlhpKIM\r\n"
"jZBgApA7QYq3sjeqs5lHzahCKftvs5XKgfxOKjxtAoGBALdnYe6xkDvGLvI51Yr+\r\n"
"Dy0TNcB5W84SxUKvM7DVEomy1QPB57ZpyQaoBq7adOz0pWJXfp7qo4950ZOhBGH1\r\n"
"hKbZ6c4ggwVJy2j49EgMok5NGCKvPAtabbR6H8Mz8DW9aXURxhWJvij+Qw1fWK4b\r\n"
"7G/qUI9iE5iUU7MkIcLIbTf/\r\n"
"-----END PRIVATE KEY-----\r\n";
const char *index_page =
"<!DOCTYPE html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"utf-8\" />\n"
" <title>WebSocket Test</title>\n"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
" <style type=\"text/css\">\n"
" body {\n"
" background-color: #789; margin: 0;\n"
" padding: 0; font: 14px Helvetica, Arial, sans-serif;\n"
" }\n"
" div.content {\n"
" width: 800px; margin: 2em auto; padding: 20px 50px;\n"
" background-color: #fff; border-radius: 1em;\n"
" }\n"
" #messages {\n"
" border: 2px solid #fec; border-radius: 1em;\n"
" height: 10em; overflow: scroll; padding: 0.5em 1em;\n"
" }\n"
" a:link, a:visited { color: #69c; text-decoration: none; }\n"
" @media (max-width: 700px) {\n"
" body { background-color: #fff; }\n"
" div.content {\n"
" width: auto; margin: 0 auto; border-radius: 0;\n"
" padding: 1em;\n"
" }\n"
" }\n"
"</style>\n"
"\n"
"<script language=\"javascript\" type=\"text/javascript\">\n"
"\n"
" var rooms = [];\n"
" var ws = new WebSocket(\'ws://\' + location.host + \'/ws\');\n"
"\n"
" if (!window.console) { window.console = { log: function() {} } };\n"
"\n"
" ws.onopen = function(ev) { console.log(ev); };\n"
" ws.onerror = function(ev) { console.log(ev); };\n"
" ws.onclose = function(ev) { console.log(ev); };\n"
" ws.onmessage = function(ev) {\n"
" console.log(ev);\n"
" var div = document.createElement(\'div\');\n"
" div.innerHTML = ev.data;\n"
" document.getElementById(\'messages\').appendChild(div);\n"
"\n"
" };\n"
"\n"
" window.onload = function() {\n"
" document.getElementById(\'send_button\').onclick = function(ev) {\n"
" var msg = document.getElementById(\'send_input\').value;\n"
" document.getElementById(\'send_input\').value = \'\';\n"
" ws.send(msg);\n"
" };\n"
" document.getElementById(\'send_input\').onkeypress = function(ev) {\n"
" if (ev.keyCode == 13 || ev.which == 13) {\n"
" document.getElementById(\'send_button\').click();\n"
" }\n"
" };\n"
" };\n"
"</script>\n"
"</head>\n"
"<body>\n"
" <div class=\"content\">\n"
" <h1>Websocket PubSub Demonstration</h1>\n"
"\n"
" <p>\n"
" This page demonstrates how Mongoose could be used to implement\n"
" <a href=\"http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern\">\n"
" publishsubscribe pattern</a>. Open this page in several browser\n"
" windows. Each window initiates persistent\n"
" <a href=\"http://en.wikipedia.org/wiki/WebSocket\">WebSocket</a>\n"
" connection with the server, making each browser window a websocket client.\n"
" Send messages, and see messages sent by other clients.\n"
" </p>\n"
"\n"
" <div id=\"messages\">\n"
" </div>\n"
"\n"
" <p>\n"
" <input type=\"text\" id=\"send_input\" />\n"
" <button id=\"send_button\">Send Message</button>\n"
" </p>\n"
" </div>\n"
"</body>\n"
"</html>\n";
#include <Arduino.h>
void broadcast(MongooseHttpWebSocketConnection *from, MongooseString msg)
{
char buf[500];
char addr[32];
mg_sock_addr_to_str(from->getRemoteAddress(), addr, sizeof(addr),
MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
snprintf(buf, sizeof(buf), "%s %.*s", addr, (int) msg.length(), msg.c_str());
printf("%s\n", buf);
server.sendAll(from, buf);
}
void setup()
{
Serial.begin(115200);
#ifdef START_ESP_WIFI
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Hostname: ");
#ifdef ESP32
Serial.println(WiFi.getHostname());
#elif defined(ESP8266)
Serial.println(WiFi.hostname());
#endif
#endif
Mongoose.begin();
#ifdef SIMPLE_SERVER_SECURE
if(false == server.begin(443, server_pem, server_key)) {
Serial.print("Failed to start server");
return;
}
#else
server.begin(80);
#endif
server.on("/$", HTTP_GET, [](MongooseHttpServerRequest *request) {
request->send(200, "text/html", index_page);
});
// Test the stream response class
server.on("/ws$")->
onConnect([](MongooseHttpWebSocketConnection *connection) {
broadcast(connection, MongooseString("++ joined"));
})->
onClose([](MongooseHttpServerRequest *c) {
MongooseHttpWebSocketConnection *connection = static_cast<MongooseHttpWebSocketConnection *>(c);
broadcast(connection, MongooseString("++ left"));
})->
onFrame([](MongooseHttpWebSocketConnection *connection, int flags, uint8_t *data, size_t len) {
broadcast(connection, MongooseString((const char *)data, len));
});
}
void loop()
{
Mongoose.poll(1000);
Serial.printf("Free memory %u\n", ESP.getFreeHeap());
}

View File

@@ -1,11 +0,0 @@
This directory is intended for PIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html

View File

@@ -1,35 +0,0 @@
# Name: REST Client
# Id: humao.rest-client
# Description: REST Client for Visual Studio Code
# Version: 0.21.3
# Publisher: Huachao Mao
# VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=humao.rest-client
@baseUrl = http://172.16.0.87
###
GET {{baseUrl}}/ HTTP/1.1
###
GET {{baseUrl}}/get?message=Hello+World HTTP/1.1
###
POST {{baseUrl}}/post HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
message=Hello+World
###
GET {{baseUrl}}/someRandomFile HTTP/1.1
###
GET {{baseUrl}}/basic HTTP/1.1
###
GET {{baseUrl}}/stream HTTP/1.1

View File

@@ -1,66 +1,73 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PsychicHTTP Demo</title>
<link rel="icon" href="./favicon.ico" type="image/x-icon">
</head>
<body>
<main>
<h1>Basic Request Examples</h1>
<ul>
<li><a href="/api?foo=bar">API Call</a></li>
<li><a href="/redirect">Redirection</a></li>
<li><a href="/auth-digest">Authentication (Digest)</a></li>
<li><a href="/auth-basic">Authentication (Basic)</a></li>
<li><a href="/cookies">Cookies Demo</a></li>
<li><a href="/404">404</a></li>
</ul>
<h1>Static Serving</h1> <head>
<p> <meta charset="UTF-8">
<a href="/alien.png"><img width="60" src="/alien.png"></a> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<a href="/img/request_flow.png"><img width="60" src="/img/request_flow.png"></a> <title>PsychicHTTP Demo</title>
</p> <link rel="icon" href="./favicon.ico" type="image/x-icon">
<p><a href="/myfile.txt">Text File</a></p> </head>
<h1>Simple POST Form</h1> <body>
<form action="/post" method="post"> <main>
<label for="param1">Parameter 1:</label> <h1>Basic Request Examples</h1>
<input type="text" id="param1" name="param1" value="Parameter 1"> <ul>
<br> <li><a href="/api?foo=bar">API Call</a></li>
<label for="param2">Parameter 2:</label> <li><a href="/redirect">Redirection</a></li>
<input type="text" id="param2" name="param2" value="Parameter 2"> <li><a href="/auth-digest">Authentication (Digest)</a></li>
<br> <li><a href="/auth-basic">Authentication (Basic)</a></li>
<input type="submit" value="Submit"> <li><a href="/cookies">Cookies Demo</a></li>
</form> <li><a href="/404">404</a></li>
</ul>
<h1>Basic File Upload</h1> <h1>Utilities</h1>
<table border="0"> <ul>
<tr> <li><a href="/websocket-test.html">WebSocket Tester</a></li>
<td> </ul>
<label for="newfile">Upload a file</label>
</td> <h1>Static Serving</h1>
<td colspan="2"> <p>
<input id="newfile" type="file" onchange="setpath()" style="width:100%;"> <a href="/alien.png"><img width="60" src="/alien.png"></a>
</td> <a href="/img/request_flow.png"><img width="60" src="/img/request_flow.png"></a>
</tr> </p>
<tr> <p><a href="/myfile.txt">Text File</a></p>
<td>
<label for="filepath">Set path on server</label> <h1>Simple POST Form</h1>
</td> <form action="/post" method="post">
<td> <label for="param1">Parameter 1:</label>
<input id="filepath" type="text" style="width:100%;"> <input type="text" id="param1" name="param1" value="Parameter 1">
</td> <br>
</tr> <label for="param2">Parameter 2:</label>
<tr> <input type="text" id="param2" name="param2" value="Parameter 2">
<td> <br>
<button id="upload" type="button" onclick="upload()">Upload</button> <input type="submit" value="Submit">
</td> </form>
</tr>
</table> <h1>Basic File Upload</h1>
<script> <table border="0">
<tr>
<td>
<label for="newfile">Upload a file</label>
</td>
<td colspan="2">
<input id="newfile" type="file" onchange="setpath()" style="width:100%;">
</td>
</tr>
<tr>
<td>
<label for="filepath">Set path on server</label>
</td>
<td>
<input id="filepath" type="text" style="width:100%;">
</td>
</tr>
<tr>
<td>
<button id="upload" type="button" onclick="upload()">Upload</button>
</td>
</tr>
</table>
<script>
function setpath() { function setpath() {
var default_path = document.getElementById("newfile").files[0].name; var default_path = document.getElementById("newfile").files[0].name;
document.getElementById("filepath").value = default_path; document.getElementById("filepath").value = default_path;
@@ -72,7 +79,7 @@
/* Max size of an individual file. Make sure this /* Max size of an individual file. Make sure this
* value is same as that set in file_server.c */ * value is same as that set in file_server.c */
var MAX_FILE_SIZE = 2048*1024; var MAX_FILE_SIZE = 2048 * 1024;
var MAX_FILE_SIZE_STR = "2MB"; var MAX_FILE_SIZE_STR = "2MB";
if (fileInput.length == 0) { if (fileInput.length == 0) {
@@ -81,9 +88,9 @@
alert("File path on server is not set!"); alert("File path on server is not set!");
} else if (filePath.indexOf(' ') >= 0) { } else if (filePath.indexOf(' ') >= 0) {
alert("File path on server cannot have spaces!"); alert("File path on server cannot have spaces!");
} else if (filePath[filePath.length-1] == '/') { } else if (filePath[filePath.length - 1] == '/') {
alert("File name not specified after path!"); alert("File name not specified after path!");
} else if (fileInput[0].size > 200*1024) { } else if (fileInput[0].size > 200 * 1024) {
alert("File size must be less than 200KB!"); alert("File size must be less than 200KB!");
} else { } else {
document.getElementById("newfile").disabled = true; document.getElementById("newfile").disabled = true;
@@ -92,7 +99,7 @@
var file = fileInput[0]; var file = fileInput[0];
var xhttp = new XMLHttpRequest(); var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function () {
if (xhttp.readyState == 4) { if (xhttp.readyState == 4) {
if (xhttp.status == 200) { if (xhttp.status == 200) {
document.open(); document.open();
@@ -111,126 +118,125 @@
xhttp.send(file); xhttp.send(file);
} }
} }
</script> </script>
<h1>Multipart POST Form</h1> <h1>Multipart POST Form</h1>
<form action="/multipart" method="post" enctype="multipart/form-data"> <form action="/multipart" method="post" enctype="multipart/form-data">
<label for="param1">Parameter 1:</label> <label for="param1">Parameter 1:</label>
<input type="text" id="param1" name="param1" value="Parameter 1"> <input type="text" id="param1" name="param1" value="Parameter 1">
<br> <br>
<label for="param2">Parameter 2:</label> <label for="param2">Parameter 2:</label>
<input type="text" id="param2" name="param2" value="Parameter 2"> <input type="text" id="param2" name="param2" value="Parameter 2">
<br> <br>
<label for="file-upload">File Upload:</label> <label for="file-upload">File Upload:</label>
<input type="file" id="file-upload" name="file_upload" accept=".txt, .html, .pdf, .png, .jpg, .gif" required> <input type="file" id="file-upload" name="file_upload" accept=".txt, .html, .pdf, .png, .jpg, .gif"
<br> required>
<br>
<input type="submit" value="Upload File">
</form>
<h1>Websocket Demo</h1> <input type="submit" value="Upload File">
<input type="text" id="message_input" placeholder="Type your message"> </form>
<button onclick="sendMessage()">Send Message</button>
<button onclick="websocketConnect()">Connect</button>
<div>
<textarea id="websocket_output" style="width: 50%; height: 250px;"></textarea>
</div>
<script> <h1>Websocket Demo</h1>
let socket; <input type="text" id="message_input" placeholder="Type your message">
const outputText = document.getElementById('websocket_output'); <button onclick="sendMessage()">Send Message</button>
const messageInput = document.getElementById('message_input'); <button onclick="websocketConnect()">Connect</button>
<div>
<textarea id="websocket_output" style="width: 50%; height: 250px;"></textarea>
</div>
function websocketConnect() <script>
{ let socket;
// Create a WebSocket connection const outputText = document.getElementById('websocket_output');
socket = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + window.location.host + "/ws"); const messageInput = document.getElementById('message_input');
// Event handler for when the WebSocket connection is open function websocketConnect() {
socket.addEventListener('open', (event) => { // Create a WebSocket connection
outputText.value += `[socket] connection opened!\n`; socket = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + window.location.host + "/ws");
outputText.scrollTop = outputText.scrollHeight;
}); // Event handler for when the WebSocket connection is open
socket.addEventListener('open', (event) => {
// Event handler for when a message is received from the WebSocket server outputText.value += `[socket] connection opened!\n`;
socket.addEventListener('message', (event) => { outputText.scrollTop = outputText.scrollHeight;
// Echo the received message into the output div });
let data = event.data.trim();
outputText.value += `[received] ${data}\n`; // Event handler for when a message is received from the WebSocket server
outputText.scrollTop = outputText.scrollHeight; socket.addEventListener('message', (event) => {
}); // Echo the received message into the output div
let data = event.data.trim();
// Event handler for when an error occurs with the WebSocket connection outputText.value += `[received] ${data}\n`;
socket.addEventListener('error', (event) => { outputText.scrollTop = outputText.scrollHeight;
let data = event.data.trim(); });
outputText.value += `[error] ${event.data}\n`;
outputText.scrollTop = outputText.scrollHeight; // Event handler for when an error occurs with the WebSocket connection
}); socket.addEventListener('error', (event) => {
let data = event.data.trim();
// Event handler for when the WebSocket connection is closed outputText.value += `[error] ${event.data}\n`;
socket.addEventListener('close', (event) => { outputText.scrollTop = outputText.scrollHeight;
outputText.value += `[socket] connection closed!\n`; });
});
} // Event handler for when the WebSocket connection is closed
socket.addEventListener('close', (event) => {
// Function to send a message to the WebSocket server outputText.value += `[socket] connection closed!\n`;
function sendMessage() { });
if (socket.readyState == WebSocket.OPEN) { }
const message = messageInput.value.trim();
if (message) { // Function to send a message to the WebSocket server
socket.send(message); function sendMessage() {
messageInput.value = ''; // Clear the input field after sending the message if (socket.readyState == WebSocket.OPEN) {
outputText.value += `[sent] ${message}\n`; const message = messageInput.value.trim();
outputText.scrollTop = outputText.scrollHeight; if (message) {
} socket.send(message);
} messageInput.value = ''; // Clear the input field after sending the message
else outputText.value += `[sent] ${message}\n`;
{
outputText.value += `[error] Not Connected\n`;
outputText.scrollTop = outputText.scrollHeight; outputText.scrollTop = outputText.scrollHeight;
} }
} }
</script> else {
outputText.value += `[error] Not Connected\n`;
<h1>EventSource Demo</h1> outputText.scrollTop = outputText.scrollHeight;
<button onclick="eventSourceConnect()">Connect</button>
<div>
<textarea id="eventsource_output" style="width: 50%; height: 250px;"></textarea>
</div>
<script>
const dataElement = document.getElementById('eventsource_output');
function eventSourceConnect()
{
const eventSource = new EventSource('/events');
eventSource.onopen = () => {
dataElement.value += `[connected]\n`;
dataElement.scrollTop = dataElement.scrollHeight;
};
eventSource.addEventListener('millis', (event) => {
let data = event.data.trim()
dataElement.value += `[millis] ${data}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
});
eventSource.onmessage = (event) => {
let data = event.data.trim()
dataElement.value += `[message] ${data}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
};
eventSource.onerror = (error) => {
dataElement.value += `[error] ${error}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
eventSource.close();
};
} }
</script> }
</main> </script>
</body>
<h1>EventSource Demo</h1>
<button onclick="eventSourceConnect()">Connect</button>
<div>
<textarea id="eventsource_output" style="width: 50%; height: 250px;"></textarea>
</div>
<script>
const dataElement = document.getElementById('eventsource_output');
function eventSourceConnect() {
const eventSource = new EventSource('/events');
eventSource.onopen = () => {
dataElement.value += `[connected]\n`;
dataElement.scrollTop = dataElement.scrollHeight;
};
eventSource.addEventListener('millis', (event) => {
let data = event.data.trim()
dataElement.value += `[millis] ${data}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
});
eventSource.onmessage = (event) => {
let data = event.data.trim()
dataElement.value += `[message] ${data}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
};
eventSource.onerror = (error) => {
dataElement.value += `[error] ${error}\n`;
dataElement.scrollTop = dataElement.scrollHeight;
eventSource.close();
};
}
</script>
</main>
</body>
</html> </html>

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Message Rate Test</title>
</head>
<body>
<h1>WebSocket Message Rate Test</h1>
<p>Time Remaining: <span id="time_remaining">0</span></p>
<p>Messages Count: <span id="message_count">0</span></p>
<p>Messages per second: <span id="rate">0</span></p>
<label for="duration">Test Duration (seconds):</label>
<input type="number" id="duration" value="30" min="1">
<p>
<button id="startTestSmall">Start Test (256b json)</button>
<button id="startTestBig">Start Test (2k json)</button>
</p>
<p id="status"></p>
<script>
let ws;
let messageCount = 0;
let startTime;
let endTime;
let testRunning = false;
let smallPayload = { "user": { "id": 123456789, "name": "JohnDoe", "email": "johndoe@example.com", "preferences": { "theme": "dark", "notifications": { "email": true, "sms": false }, "language": "en" } } };
let bigPayload = { "user": { "id": 123456789, "name": "JohnDoe", "email": "johndoe@example.com", "preferences": { "theme": "dark", "notifications": { "email": true, "sms": false }, "language": "en", "options": { "option1": "value1", "option2": "value2", "option3": "value3", "option4": "value4", "option5": "value5", "option6": "value6", "option7": "value7", "option8": "value8", "option9": "value9", "option10": "value10", "option11": "value11", "option12": "value12", "option13": "value13", "option14": "value14", "option15": "value15", "option16": "value16", "option17": "value17", "option18": "value18", "option19": "value19", "option20": "value20", "option21": "value21", "option22": "value22", "option23": "value23", "option24": "value24", "option25": "value25", "option26": "value26", "option27": "value27", "option28": "value28", "option29": "value29", "option30": "value30", "option31": "value31", "option32": "value32", "option33": "value33", "option34": "value34", "option35": "value35", "option36": "value36", "option37": "value37", "option38": "value38", "option39": "value39", "option40": "value40", "option41": "value41", "option42": "value42", "option43": "value43", "option44": "value44", "option45": "value45", "option46": "value46", "option47": "value47", "option48": "value48", "option49": "value49", "option50": "value50", "option51": "value51", "option52": "value52", "option53": "value53", "option54": "value54", "option55": "value55", "option56": "value56", "option57": "value57", "option58": "value58", "option59": "value59", "option60": "value60", "option61": "value61", "option62": "value62", "option63": "value63", "option64": "value64", "option65": "value65", "option66": "value66", "option67": "value67", "option68": "value68", "option69": "value69", "option70": "value70", "option71": "value71", "option72": "value72", "option73": "value73", "option74": "value74", "option75": "value75", "option76": "value76", "option77": "value77", "option78": "value78", "option79": "value79", "option80": "value80", "option81": "value81", "option82": "value82", "option83": "value83", "option84": "value84", "option85": "value85", "option86": "value86", "option87": "value87", "option88": "value88", "option89": "value89", "option90": "value90", "option91": "value91", "option92": "value92", "option93": "value93", "option94": "value94", "option95": "value95", "option96": "value96", "option97": "value97", "option98": "value98", "option99": "value99", "option100": "value100", "option101": "value101", "option102": "value102", "option103": "value103", "option104": "value104", "option105": "value105", "option106": "value106", "option107": "value107", "option108": "value108", "option109": "value109", "option110": "value110", "option111": "value111", "option112": "value112", "option113": "value113", "option114": "value114", "option115": "value115", "option116": "value116", "option117": "value117", "option118": "value118", "option119": "value119", "option120": "value120" } } } };
let payload;
// Function to update the message rate
function updateRate(force = false) {
const currentTime = Date.now();
const elapsedTime = (currentTime - startTime) / 1000; // in seconds
let remainingTime = (endTime - currentTime) / 1000; // in seconds
remainingTime = Math.max(0, remainingTime);
const rate = messageCount / elapsedTime;
if (rate) {
document.getElementById('rate').innerText = rate.toFixed(2);
document.getElementById('message_count').innerText = messageCount;
document.getElementById('time_remaining').innerText = remainingTime.toFixed(2);
}
if (testRunning)
setTimeout(updateRate, 25);
}
function startTestSmall() {
payload = smallPayload;
startTest();
}
function startTestBig() {
payload = bigPayload;
startTest();
}
// Function to start the WebSocket connection and the test
function startTest() {
if (testRunning) return;
console.log("Payload length: " + JSON.stringify(payload).length);
document.getElementById('startTestSmall').disabled = true;
document.getElementById('startTestBig').disabled = true;
document.getElementById('status').innerText = 'Connecting';
const durationInput = document.getElementById('duration').value;
const testDuration = parseInt(durationInput) * 1000 || 60000; // default to 60 seconds if invalid
// Determine the WebSocket protocol based on the current protocol
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(`${protocol}${window.location.host}/ws`);
ws.onopen = function () {
document.getElementById('status').innerText = 'Connected';
startTime = Date.now();
endTime = startTime + testDuration;
messageCount = 0;
testRunning = true;
sendAndReceiveMessage();
updateRate();
};
ws.onmessage = function (event) {
try {
const parsedData = JSON.parse(event.data);
if (parsedData.user) {
messageCount++;
if (testRunning) {
sendAndReceiveMessage();
}
}
} catch (err) { }
};
ws.onerror = function (error) {
document.getElementById('status').innerText = 'Error: ' + error.message;
};
ws.onclose = function () {
//document.getElementById('status').innerText = 'Connection Closed';
};
}
// Function to send a message and wait for the response
function sendAndReceiveMessage() {
if (Date.now() >= endTime) {
testRunning = false;
document.getElementById('startTestSmall').disabled = false;
document.getElementById('startTestBig').disabled = false;
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
document.getElementById('status').innerText = 'Test Finished';
updateRate(true);
} else {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
document.getElementById('startTestSmall').addEventListener('click', startTestSmall);
document.getElementById('startTestBig').addEventListener('click', startTestBig);
</script>
</body>
</html>

View File

@@ -1,39 +0,0 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

View File

@@ -1,46 +0,0 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -12,19 +12,44 @@
platform = espressif32 platform = espressif32
framework = arduino framework = arduino
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_port = /dev/ttyACM0
monitor_port = /dev/ttyACM1
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
lib_deps = lib_deps =
; devmode: with this disabled make a symlink from platformio/lib to the PsychicHttp directory ; hoeken/PsychicHttp
;hoeken/PsychicHttp ; PIO is not able to consider installed project in CI
bblanchon/ArduinoJson ;../..
board_build.filesystem = littlefs board_build.filesystem = littlefs
[env:default]
build_flags = build_flags =
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_WARN -Wall
;-D ENABLE_ASYNC -Wextra
; [env:arduino3] [env:arduino2]
; platform = https://github.com/platformio/platform-espressif32.git platform = espressif32@6.8.1
; platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32#master
[env:arduino2-ssl]
platform = espressif32@6.8.1
build_flags = -D PSY_ENABLE_SSL
[env:arduino2-regex]
platform = espressif32@6.8.1
build_flags = -D PSY_ENABLE_REGEX
[env:arduino3]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
[env:arduino3-ssl]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
build_flags = -D PSY_ENABLE_SSL
[env:arduino3-regex]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
build_flags = -D PSY_ENABLE_REGEX
[env:waveshare-4-3-touchscreen]
lib_deps = ${env.lib_deps}
https://github.com/esp-arduino-libs/ESP32_IO_Expander
build_flags =
-D PSY_ENABLE_SDCARD
-D WAVESHARE_43_TOUCH

View File

@@ -9,66 +9,113 @@
*/ */
/********************************************************************************************** /**********************************************************************************************
* Note: this demo relies on various files to be uploaded on the LittleFS partition * Note: this demo relies on various files to be uploaded on the LittleFS partition
* PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image * PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image
**********************************************************************************************/ **********************************************************************************************/
#include "_secret.h"
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h>
#include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <esp_sntp.h> #include <LittleFS.h>
#include "_secret.h"
#include <PsychicHttp.h> #include <PsychicHttp.h>
//#include <PsychicHttpsServer.h> //uncomment this to enable HTTPS / SSL #include <WiFi.h>
#include <esp_sntp.h>
// #define this to enable SD card support
#ifdef PSY_ENABLE_SDCARD
#ifdef WAVESHARE_43_TOUCH
#include <ESP_IOExpander_Library.h>
// Extend IO Pin define
#define TP_RST 1
#define LCD_BL 2
#define LCD_RST 3
#define SD_CS 4
#define USB_SEL 5
// I2C Pin define
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_SDA_IO 8
#define I2C_MASTER_SCL_IO 9
#define SD_MOSI 11
#define SD_CLK 12
#define SD_MISO 13
#define SD_SS -1
#else
#define SD_MOSI 11
#define SD_CLK 12
#define SD_MISO 13
#define SD_SS 5
#endif
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#endif
// #define this to enable SSL at build (or switch to the 'ssl' build target in vscode)
#ifdef PSY_ENABLE_SSL
#include <PsychicHttpsServer.h>
#endif
// debugging library
#ifdef PSY_DEVMODE
#include <ArduinoTrace.h>
#endif
#ifndef WIFI_SSID #ifndef WIFI_SSID
#error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there."
#endif #endif
//Enter your WIFI credentials in secret.h // Enter your WIFI credentials in secret.h
const char *ssid = WIFI_SSID; const char* ssid = WIFI_SSID;
const char *password = WIFI_PASS; const char* password = WIFI_PASS;
// Set your SoftAP credentials // Set your SoftAP credentials
const char *softap_ssid = "PsychicHttp"; const char* softap_ssid = "PsychicHttp";
const char *softap_password = ""; const char* softap_password = "";
IPAddress softap_ip(10, 0, 0, 1); IPAddress softap_ip(10, 0, 0, 1);
//credentials for the /auth-basic and /auth-digest examples // credentials for the /auth-basic and /auth-digest examples
const char *app_user = "admin"; const char* app_user = "admin";
const char *app_pass = "admin"; const char* app_pass = "admin";
const char *app_name = "Your App"; const char* app_name = "Your App";
//hostname for mdns (psychic.local) LoggingMiddleware loggingMiddleware;
const char *local_hostname = "psychic"; AuthenticationMiddleware basicAuth;
AuthenticationMiddleware digestAuth;
//#define PSY_ENABLE_SSL to enable ssl // hostname for mdns (psychic.local)
const char* local_hostname = "psychic";
// #define PSY_ENABLE_SSL to enable ssl
#ifdef PSY_ENABLE_SSL #ifdef PSY_ENABLE_SSL
bool app_enable_ssl = true; bool app_enable_ssl = true;
String server_cert; String server_cert;
String server_key; String server_key;
#endif #endif
//our main server object // our main server object
#ifdef PSY_ENABLE_SSL #ifdef PSY_ENABLE_SSL
PsychicHttpsServer server; PsychicHttpsServer server;
#else #else
PsychicHttpServer server; PsychicHttpServer server;
#endif #endif
PsychicWebSocketHandler websocketHandler; PsychicWebSocketHandler websocketHandler;
PsychicEventSource eventSource; PsychicEventSource eventSource;
CorsMiddleware corsMiddleware;
//NTP server stuff // NTP server stuff
const char *ntpServer1 = "pool.ntp.org"; const char* ntpServer1 = "pool.ntp.org";
const char *ntpServer2 = "time.nist.gov"; const char* ntpServer2 = "time.nist.gov";
const long gmtOffset_sec = 0; const long gmtOffset_sec = 0;
const int daylightOffset_sec = 0; const int daylightOffset_sec = 0;
struct tm timeinfo; struct tm timeinfo;
// Callback function (gets called when time adjusts via NTP) // Callback function (gets called when time adjusts via NTP)
void timeAvailable(struct timeval *t) void timeAvailable(struct timeval* t)
{ {
if (!getLocalTime(&timeinfo)) { if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time"); Serial.println("Failed to obtain time");
@@ -83,10 +130,12 @@ void timeAvailable(struct timeval *t)
bool connectToWifi() bool connectToWifi()
{ {
//dual client and AP mode // WiFi.mode(WIFI_AP); // ap only mode
WiFi.mode(WIFI_AP_STA); // WiFi.mode(WIFI_STA); // client only mode
WiFi.mode(WIFI_AP_STA); // ap and client
// Configure SoftAP // Configure SoftAP
// dual client and AP mode
WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00
WiFi.softAP(softap_ssid, softap_password); WiFi.softAP(softap_ssid, softap_password);
IPAddress myIP = WiFi.softAPIP(); IPAddress myIP = WiFi.softAPIP();
@@ -107,10 +156,8 @@ bool connectToWifi()
int numberOfTries = 20; int numberOfTries = 20;
// Wait for the WiFi event // Wait for the WiFi event
while (true) while (true) {
{ switch (WiFi.status()) {
switch (WiFi.status())
{
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
Serial.println("[WiFi] SSID not found"); Serial.println("[WiFi] SSID not found");
break; break;
@@ -140,15 +187,12 @@ bool connectToWifi()
} }
delay(tryDelay); delay(tryDelay);
if (numberOfTries <= 0) if (numberOfTries <= 0) {
{
Serial.print("[WiFi] Failed to connect to WiFi!"); Serial.print("[WiFi] Failed to connect to WiFi!");
// Use disconnect function to force stop trying to connect // Use disconnect function to force stop trying to connect
WiFi.disconnect(); WiFi.disconnect();
return false; return false;
} } else {
else
{
numberOfTries--; numberOfTries--;
} }
} }
@@ -156,198 +200,247 @@ bool connectToWifi()
return false; return false;
} }
#ifdef PSY_ENABLE_SDCARD
bool setupSDCard()
{
#ifdef WAVESHARE_43_TOUCH
ESP_IOExpander* expander = new ESP_IOExpander_CH422G((i2c_port_t)I2C_MASTER_NUM, ESP_IO_EXPANDER_I2C_CH422G_ADDRESS_000, I2C_MASTER_SCL_IO, I2C_MASTER_SDA_IO);
expander->init();
expander->begin();
expander->multiPinMode(TP_RST | LCD_BL | LCD_RST | SD_CS | USB_SEL, OUTPUT);
expander->multiDigitalWrite(TP_RST | LCD_BL | LCD_RST, HIGH);
// use extend GPIO for SD card
expander->digitalWrite(SD_CS, LOW);
SPI.setHwCs(false);
#endif
SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_SS);
if (!SD.begin()) {
Serial.println("SD Card Mount Failed");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD card attached");
return false;
}
Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
return true;
}
#endif
void setup() void setup()
{ {
esp_log_level_set(PH_TAG, ESP_LOG_DEBUG);
esp_log_level_set("httpd_uri", ESP_LOG_DEBUG);
Serial.begin(115200); Serial.begin(115200);
delay(10); delay(10);
Serial.printf("ESP-IDF Version: %s\n", esp_get_idf_version());
#ifdef ESP_ARDUINO_VERSION_STR
Serial.printf("Arduino Version: %s\n", ESP_ARDUINO_VERSION_STR);
#else
Serial.printf("Arduino Version: %d.%d.%d\n", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH);
#endif
Serial.printf("PsychicHttp Version: %s\n", PSYCHIC_VERSION_STR);
// We start by connecting to a WiFi network // We start by connecting to a WiFi network
// To debug, please enable Core Debug Level to Verbose // To debug, please enable Core Debug Level to Verbose
if (connectToWifi()) if (connectToWifi()) {
{ // Setup our NTP to get the current time.
//Setup our NTP to get the current time.
sntp_set_time_sync_notification_cb(timeAvailable); sntp_set_time_sync_notification_cb(timeAvailable);
sntp_servermode_dhcp(1); // (optional) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2)
esp_sntp_servermode_dhcp(1); // (optional)
#else
sntp_servermode_dhcp(1); // (optional)
#endif
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
//set up our esp32 to listen on the local_hostname.local domain // set up our esp32 to listen on the psychic.local domain
if (!MDNS.begin(local_hostname)) { if (MDNS.begin(local_hostname))
MDNS.addService("http", "tcp", 80);
else
Serial.println("Error starting mDNS"); Serial.println("Error starting mDNS");
return;
}
MDNS.addService("http", "tcp", 80);
if(!LittleFS.begin()) if (!LittleFS.begin()) {
{
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
//look up our keys? #ifdef PSY_ENABLE_SSL
#ifdef PSY_ENABLE_SSL // look up our keys?
if (app_enable_ssl) if (app_enable_ssl) {
{ File fp = LittleFS.open("/server.crt");
File fp = LittleFS.open("/server.crt"); if (fp) {
if (fp) server_cert = fp.readString();
{
server_cert = fp.readString();
// Serial.println("Server Cert:"); // Serial.println("Server Cert:");
// Serial.println(server_cert); // Serial.println(server_cert);
} } else {
else Serial.println("server.pem not found, SSL not available");
{ app_enable_ssl = false;
Serial.println("server.pem not found, SSL not available");
app_enable_ssl = false;
}
fp.close();
File fp2 = LittleFS.open("/server.key");
if (fp2)
{
server_key = fp2.readString();
// Serial.println("Server Key:");
// Serial.println(server_key);
}
else
{
Serial.println("server.key not found, SSL not available");
app_enable_ssl = false;
}
fp2.close();
} }
#endif fp.close();
//setup server config stuff here File fp2 = LittleFS.open("/server.key");
server.config.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) if (fp2) {
server_key = fp2.readString();
#ifdef PSY_ENABLE_SSL // Serial.println("Server Key:");
server.ssl_config.httpd.max_uri_handlers = 20; //maximum number of uri handlers (.on() calls) // Serial.println(server_key);
} else {
//do we want secure or not? Serial.println("server.key not found, SSL not available");
if (app_enable_ssl) app_enable_ssl = false;
{
server.listen(443, server_cert.c_str(), server_key.c_str());
//this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer *redirectServer = new PsychicHttpServer();
redirectServer->config.ctrl_port = 20424; // just a random port different from the default one
redirectServer->listen(80);
redirectServer->onNotFound([](PsychicRequest *request) {
String url = "https://" + request->host() + request->url();
return request->redirect(url.c_str());
});
} }
else fp2.close();
server.listen(80); }
#else
server.listen(80); // do we want secure or not?
#endif if (app_enable_ssl) {
server.setCertificate(server_cert.c_str(), server_key.c_str());
// this creates a 2nd server listening on port 80 and redirects all requests HTTPS
PsychicHttpServer* redirectServer = new PsychicHttpServer();
redirectServer->config.ctrl_port = 20424; // just a random port different from the default one
redirectServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) {
String url = "https://" + request->host() + request->url();
return response->redirect(url.c_str()); });
}
#endif
DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); DefaultHeaders::Instance().addHeader("Server", "PsychicHttp");
//serve static files from LittleFS/www on / only to clients on same wifi network loggingMiddleware.setOutput(Serial);
//this is where our /index.html file lives
// curl -i http://psychic.local/
PsychicStaticFileHandler* handler = server.serveStatic("/", LittleFS, "/www/");
handler->setFilter(ON_STA_FILTER);
handler->setCacheControl("max-age=60");
//serve static files from LittleFS/www-ap on / only to clients on SoftAP basicAuth.setUsername(app_user);
//this is where our /index.html file lives basicAuth.setPassword(app_pass);
server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER); basicAuth.setRealm(app_name);
basicAuth.setAuthMethod(HTTPAuthMethod::BASIC_AUTH);
basicAuth.setAuthFailureMessage("You must log in.");
//serve static files from LittleFS/img on /img digestAuth.setUsername(app_user);
//it's more efficient to serve everything from a single www directory, but this is also possible. digestAuth.setPassword(app_pass);
// curl -i http://psychic.local/img/request_flow.png digestAuth.setRealm(app_name);
digestAuth.setAuthMethod(HTTPAuthMethod::DIGEST_AUTH);
digestAuth.setAuthFailureMessage("You must log in.");
// corsMiddleware.setAllowCredentials(true);
// corsMiddleware.setOrigin("http://www.example.com,https://www.example.com,http://api.example.com,https://api.example.com");
// corsMiddleware.setHeaders("Origin,X-Requested-With,Content-Type,Accept,Content-Type,Authorization,X-Access-Token");
server.addMiddleware(&loggingMiddleware);
// this will send CORS headers on every HTTP_OPTIONS request that contains the Origin: header
server.addMiddleware(&corsMiddleware);
// rewrites!
server.rewrite("/rewrite", "/api?foo=rewrite");
// serve static files from LittleFS/www on / only to clients on same wifi network
// this is where our /index.html file lives
// curl -i http://psychic.local/
server.serveStatic("/", LittleFS, "/www/")
->setCacheControl("max-age=60")
->addFilter(ON_STA_FILTER);
// serve static files from LittleFS/www-ap on / only to clients on SoftAP
// this is where our /index.html file lives
server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER);
// serve static files from LittleFS/img on /img
// it's more efficient to serve everything from a single www directory, but this is also possible.
// curl -i http://psychic.local/img/request_flow.png
server.serveStatic("/img", LittleFS, "/img/"); server.serveStatic("/img", LittleFS, "/img/");
//you can also serve single files #ifdef PSY_ENABLE_SDCARD
// curl -i http://psychic.local/myfile.txt // if we detect an SD card, serve all files from sd:/ on http://psychic.local/sd
if (setupSDCard())
server.serveStatic("/sd", SD, "/");
#endif
// you can also serve single files
// curl -i http://psychic.local/myfile.txt
server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); server.serveStatic("/myfile.txt", LittleFS, "/custom.txt");
//example callback everytime a connection is opened // example callback everytime a connection is opened
server.onOpen([](PsychicClient *client) { server.onOpen([](PsychicClient* client) { Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); });
Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString());
});
//example callback everytime a connection is closed // example callback everytime a connection is closed
server.onClose([](PsychicClient *client) { server.onClose([](PsychicClient* client) { Serial.printf("[http] connection #%u closed\n", client->socket()); });
Serial.printf("[http] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});
//api - json message passed in as post body // api - json message passed in as post body
// curl -i -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://psychic.local/api // curl -i -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://psychic.local/api
server.on("/api", HTTP_POST, [](PsychicRequest *request, JsonVariant &json) server.on("/api", HTTP_POST, [](PsychicRequest* request, PsychicResponse* resp, JsonVariant& json) {
{
JsonObject input = json.as<JsonObject>(); JsonObject input = json.as<JsonObject>();
//create our response json // create our response json
PsychicJsonResponse response = PsychicJsonResponse(request); PsychicJsonResponse response(resp);
JsonObject output = response.getRoot(); JsonObject output = response.getRoot();
output["msg"] = "status"; output["msg"] = "status";
output["status"] = "success"; output["status"] = "success";
output["millis"] = millis(); output["millis"] = millis();
output["method"] = request->methodStr();
//work with some params // work with some params
if (input.containsKey("foo")) if (input.containsKey("foo")) {
{
String foo = input["foo"]; String foo = input["foo"];
output["foo"] = foo; output["foo"] = foo;
} }
return response.send(); return response.send();
}); });
//ip - get info about the client // ip - get info about the client
// curl -i http://psychic.local/ip // curl -i http://psychic.local/ip
server.on("/ip", HTTP_GET, [](PsychicRequest *request) server.on("/ip", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{
String output = "Your IP is: " + request->client()->remoteIP().toString(); String output = "Your IP is: " + request->client()->remoteIP().toString();
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//client connect/disconnect to a url // client connect/disconnect to a url
// curl -i http://psychic.local/handler // curl -i http://psychic.local/handler
PsychicWebHandler *connectionHandler = new PsychicWebHandler(); PsychicWebHandler* connectionHandler = new PsychicWebHandler();
connectionHandler->onRequest([](PsychicRequest *request) connectionHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { return response->send("OK"); });
{ connectionHandler->onOpen([](PsychicClient* client) { Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); });
return request->reply("OK"); connectionHandler->onClose([](PsychicClient* client) { Serial.printf("[handler] connection #%u closed\n", client->socket()); });
});
connectionHandler->onOpen([](PsychicClient *client) {
Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString());
});
connectionHandler->onClose([](PsychicClient *client) {
Serial.printf("[handler] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});
//add it to our server // add it to our server
server.on("/handler", connectionHandler); server.on("/handler", connectionHandler);
//api - parameters passed in via query eg. /api?foo=bar // api - parameters passed in via query eg. /api?foo=bar
// curl -i 'http://psychic.local/api?foo=bar' // curl -i 'http://psychic.local/api?foo=bar'
server.on("/api", HTTP_GET, [](PsychicRequest *request) server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* resp) {
{ // create our response json
//showcase some of the variables PsychicJsonResponse response = PsychicJsonResponse(resp);
Serial.println(request->host());
Serial.println(request->uri());
Serial.println(request->path());
Serial.println(request->queryString());
//create a response object
//create our response json
PsychicJsonResponse response = PsychicJsonResponse(request);
JsonObject output = response.getRoot(); JsonObject output = response.getRoot();
output["msg"] = "status"; output["msg"] = "status";
output["status"] = "success"; output["status"] = "success";
output["millis"] = millis(); output["millis"] = millis();
output["method"] = request->methodStr();
//work with some params // work with some params
if (request->hasParam("foo")) if (request->hasParam("foo")) {
{
String foo = request->getParam("foo")->value(); String foo = request->getParam("foo")->value();
output["foo"] = foo; output["foo"] = foo;
} }
@@ -355,112 +448,135 @@ void setup()
return response.send(); return response.send();
}); });
//JsonResponse example // curl -i -X GET 'http://psychic.local/any'
// curl -i http://psychic.local/json // curl -i -X POST 'http://psychic.local/any'
server.on("/json", HTTP_GET, [](PsychicRequest *request) server.on("/any", HTTP_ANY, [](PsychicRequest* request, PsychicResponse* resp) {
{ // create our response json
PsychicJsonResponse response = PsychicJsonResponse(request); PsychicJsonResponse response = PsychicJsonResponse(resp);
JsonObject output = response.getRoot();
output["msg"] = "status";
output["status"] = "success";
output["millis"] = millis();
output["method"] = request->methodStr();
return response.send();
});
// curl -i 'http://psychic.local/simple'
server.on("/simple", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
return response->send("Simple");
})
->setURIMatchFunction(MATCH_SIMPLE);
#ifdef PSY_ENABLE_REGEX
// curl -i 'http://psychic.local/regex/23'
// curl -i 'http://psychic.local/regex/4223'
server.on("^/regex/([\\d]+)/?$", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
// look up our regex matches
std::smatch matches;
if (request->getRegexMatches(matches)) {
String output;
output += "Matches: " + String(matches.size()) + "<br/>\n";
output += "Matched URI: " + String(matches.str(0).c_str()) + "<br/>\n";
output += "Match 1: " + String(matches.str(1).c_str()) + "<br/>\n";
return response->send(output.c_str());
} else
return response->send("No regex match.");
})
->setURIMatchFunction(MATCH_REGEX);
#endif
// JsonResponse example
// curl -i http://psychic.local/json
server.on("/json", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
PsychicJsonResponse jsonResponse = PsychicJsonResponse(response);
char key[16]; char key[16];
char value[32]; char value[32];
JsonObject root = response.getRoot(); JsonObject root = jsonResponse.getRoot();
for (int i=0; i<100; i++) for (int i = 0; i < 100; i++) {
{
sprintf(key, "key%d", i); sprintf(key, "key%d", i);
sprintf(value, "value is %d", i); sprintf(value, "value is %d", i);
root[key] = value; root[key] = value;
} }
return response.send(); return jsonResponse.send();
});
//how to redirect a request
// curl -i http://psychic.local/redirect
server.on("/redirect", HTTP_GET, [](PsychicRequest *request)
{
return request->redirect("/alien.png");
}); });
//how to do basic auth // how to redirect a request
// curl -i --user admin:admin http://psychic.local/auth-basic // curl -i http://psychic.local/redirect
server.on("/auth-basic", HTTP_GET, [](PsychicRequest *request) server.on("/redirect", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/alien.png"); });
{
if (!request->authenticate(app_user, app_pass))
return request->requestAuthentication(BASIC_AUTH, app_name, "You must log in.");
return request->reply("Auth Basic Success!");
});
//how to do digest auth // how to do basic auth
// curl -i --user admin:admin http://psychic.local/auth-digest // curl -i --user admin:admin http://psychic.local/auth-basic
server.on("/auth-digest", HTTP_GET, [](PsychicRequest *request) server.on("/auth-basic", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{ return response->send("Auth Basic Success!");
if (!request->authenticate(app_user, app_pass)) })->addMiddleware(&basicAuth);
return request->requestAuthentication(DIGEST_AUTH, app_name, "You must log in.");
return request->reply("Auth Digest Success!");
});
//example of getting / setting cookies // how to do digest auth
// curl -i -b cookie.txt -c cookie.txt http://psychic.local/cookies // curl -i --user admin:admin http://psychic.local/auth-digest
server.on("/cookies", HTTP_GET, [](PsychicRequest *request) server.on("/auth-digest", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
{ return response->send("Auth Digest Success!");
PsychicResponse response(request); })->addMiddleware(&digestAuth);
// example of getting / setting cookies
// curl -i -b cookie.txt -c cookie.txt http://psychic.local/cookies
server.on("/cookies", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) {
int counter = 0; int counter = 0;
if (request->hasCookie("counter")) char cookie[14];
{ size_t size = sizeof(cookie);
counter = std::stoi(request->getCookie("counter").c_str()); if (request->getCookie("counter", cookie, &size) == ESP_OK) {
// value is null-terminated.
counter = std::stoi(cookie);
counter++; counter++;
} }
sprintf(cookie, "%d", counter);
char cookie[10]; response->setCookie("counter", cookie);
sprintf(cookie, "%i", counter); response->setContent(cookie);
return response->send();
response.setCookie("counter", cookie);
response.setContent(cookie);
return response.send();
}); });
//example of getting POST variables // example of getting POST variables
// curl -i -d "param1=value1&param2=value2" -X POST http://psychic.local/post // curl -i -d "param1=value1&param2=value2" -X POST http://psychic.local/post
server.on("/post", HTTP_POST, [](PsychicRequest *request) // curl -F "param1=value1" -F "param2=value2" -X POST http://psychic.local/post
{ server.on("/post", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) {
String output; String output;
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n"; output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n"; output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//you can set up a custom 404 handler. // you can set up a custom 404 handler.
// curl -i http://psychic.local/404 // curl -i http://psychic.local/404
server.onNotFound([](PsychicRequest *request) server.onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "Custom 404 Handler"); });
{
return request->reply(404, "text/html", "Custom 404 Handler");
});
//handle a very basic upload as post body // handle a very basic upload as post body
PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); PsychicUploadHandler* uploadHandler = new PsychicUploadHandler();
uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { uploadHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) {
File file; File file;
String path = "/www/" + filename; String path = "/www/" + filename;
Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); Serial.printf("Writing %d/%d bytes to: %s\n", (int)index + (int)len, request->contentLength(), path.c_str());
if (last) if (last)
Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len);
//our first call? // our first call?
if (!index) if (!index)
file = LittleFS.open(path, FILE_WRITE); file = LittleFS.open(path, FILE_WRITE);
else else
file = LittleFS.open(path, FILE_APPEND); file = LittleFS.open(path, FILE_APPEND);
if(!file) { if (!file) {
Serial.println("Failed to open file"); Serial.println("Failed to open file");
return ESP_FAIL; return ESP_FAIL;
} }
if(!file.write(data, len)) { if (!file.write(data, len)) {
Serial.println("Write failed"); Serial.println("Write failed");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -468,42 +584,41 @@ void setup()
return ESP_OK; return ESP_OK;
}); });
//gets called after upload has been handled // gets called after upload has been handled
uploadHandler->onRequest([](PsychicRequest *request) uploadHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) {
{
String url = "/" + request->getFilename(); String url = "/" + request->getFilename();
String output = "<a href=\"" + url + "\">" + url + "</a>"; String output = "<a href=\"" + url + "\">" + url + "</a>";
return request->reply(output.c_str()); return response->send(output.c_str());
}); });
//wildcard basic file upload - POST to /upload/filename.ext // wildcard basic file upload - POST to /upload/filename.ext
// use http://psychic.local/ to test // use http://psychic.local/ to test
server.on("/upload/*", HTTP_POST, uploadHandler); server.on("/upload/*", HTTP_POST, uploadHandler);
//a little bit more complicated multipart form // a little bit more complicated multipart form
PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); PsychicUploadHandler* multipartHandler = new PsychicUploadHandler();
multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { multipartHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) {
File file; File file;
String path = "/www/" + filename; String path = "/www/" + filename;
//some progress over serial. // some progress over serial.
Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); Serial.printf("Writing %d bytes to: %s @ index %llu\n", (int)len, path.c_str(), index);
if (last) if (last)
Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len); Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len);
//our first call? // our first call?
if (!index) if (!index)
file = LittleFS.open(path, FILE_WRITE); file = LittleFS.open(path, FILE_WRITE);
else else
file = LittleFS.open(path, FILE_APPEND); file = LittleFS.open(path, FILE_APPEND);
if(!file) { if (!file) {
Serial.println("Failed to open file"); Serial.println("Failed to open file");
return ESP_FAIL; return ESP_FAIL;
} }
if(!file.write(data, len)) { if (!file.write(data, len)) {
Serial.println("Write failed"); Serial.println("Write failed");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -511,57 +626,83 @@ void setup()
return ESP_OK; return ESP_OK;
}); });
//gets called after upload has been handled // gets called after upload has been handled
multipartHandler->onRequest([](PsychicRequest *request) multipartHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) {
{ String output;
if (request->hasParam("file_upload")) if (request->hasParam("file_upload")) {
{ PsychicWebParameter* file = request->getParam("file_upload");
PsychicWebParameter *file = request->getParam("file_upload");
String url = "/" + file->value(); String url = "/" + file->value();
String output;
output += "<a href=\"" + url + "\">" + url + "</a><br/>\n"; output += "<a href=\"" + url + "\">" + url + "</a><br/>\n";
output += "Bytes: " + String(file->size()) + "<br/>\n"; output += "Bytes: " + String(file->size()) + "<br/>\n";
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return request->reply(output.c_str());
} }
else
return request->reply("No upload."); if (request->hasParam("param1"))
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
if (request->hasParam("param2"))
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return response->send(output.c_str());
}); });
//wildcard basic file upload - POST to /upload/filename.ext // wildcard basic file upload - POST to /upload/filename.ext
// use http://psychic.local/ to test // use http://psychic.local/ to test
// just multipart data: curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart
server.on("/multipart", HTTP_POST, multipartHandler); server.on("/multipart", HTTP_POST, multipartHandler);
//a websocket echo server // form only multipart handler
// npm install -g wscat // curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart-data
// wscat -c ws://psychic.local/ws PsychicUploadHandler* multipartFormHandler = new PsychicUploadHandler();
websocketHandler.onOpen([](PsychicWebSocketClient *client) { multipartFormHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) {
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); String output;
if (request->hasParam("param1"))
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
if (request->hasParam("param2"))
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
return response->send(output.c_str());
});
server.on("/multipart-data", HTTP_POST, multipartFormHandler);
// a websocket echo server
// npm install -g wscat
// Plaintext: wscat -c ws://psychic.local/ws
// SSL: wscat -n -c wss://psychic.local/ws
websocketHandler.onOpen([](PsychicWebSocketClient* client) {
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str());
client->sendMessage("Hello!"); client->sendMessage("Hello!");
}); });
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) {
Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); // Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), String((char*)frame->payload, frame->len).c_str());
return request->reply(frame); return request->reply(frame);
}); });
websocketHandler.onClose([](PsychicWebSocketClient *client) { websocketHandler.onClose([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u closed\n", client->socket()); });
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
//EventSource server // EventSource server
// curl -i -N http://psychic.local/events // curl -i -N http://psychic.local/events
eventSource.onOpen([](PsychicEventSourceClient *client) { eventSource.onOpen([](PsychicEventSourceClient* client) {
Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str());
client->send("Hello user!", NULL, millis(), 1000); client->send("Hello user!", NULL, millis(), 1000);
}); });
eventSource.onClose([](PsychicEventSourceClient *client) { eventSource.onClose([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u closed\n", client->socket()); });
Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});
server.on("/events", &eventSource); server.on("/events", &eventSource);
// example of using POST data inside the filter
// works: curl -F "secret=password" http://psychic.local/post-filter
// 404: curl -F "foo=bar" http://psychic.local/post-filter
server.on("/post-filter", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) {
String output;
output += "Secret: " + request->getParam("secret")->value() + "<br/>\n";
return response->send(output.c_str());
})
->addFilter([](PsychicRequest* request) {
request->loadParams();
return request->hasParam("secret");
});
server.begin();
} }
} }
@@ -570,14 +711,21 @@ char output[60];
void loop() void loop()
{ {
if (millis() - lastUpdate > 2000) if (millis() - lastUpdate > 1000) {
{ sprintf(output, "Millis: %lu\n", millis());
sprintf(output, "Millis: %d\n", millis());
websocketHandler.sendAll(output); websocketHandler.sendAll(output);
sprintf(output, "%d", millis()); sprintf(output, "%lu", millis());
eventSource.send(output, "millis", millis(), 0); eventSource.send(output, "millis", millis(), 0);
lastUpdate = millis(); lastUpdate = millis();
} }
// just some dev code to test that starting / stopping the server works okay.
// delay(5000);
// Serial.println("Stopping Server");
// server.stop();
// delay(5000);
// Serial.println("Starting Server");
// server.start();
} }

View File

@@ -1,11 +0,0 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

View File

@@ -9,38 +9,39 @@
*/ */
/********************************************************************************************** /**********************************************************************************************
* Note: this demo relies on various files to be uploaded on the LittleFS partition * Note: this demo relies on various files to be uploaded on the LittleFS partition
* PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image * PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image
**********************************************************************************************/ **********************************************************************************************/
#include "_secret.h"
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h>
#include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <esp_sntp.h> #include <LittleFS.h>
#include "_secret.h"
#include <PsychicHttp.h> #include <PsychicHttp.h>
#include <WiFi.h>
#include <esp_sntp.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#ifndef WIFI_SSID #ifndef WIFI_SSID
#error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there."
#endif #endif
//Enter your WIFI credentials in secret.h // Enter your WIFI credentials in secret.h
const char *ssid = WIFI_SSID; const char* ssid = WIFI_SSID;
const char *password = WIFI_PASS; const char* password = WIFI_PASS;
//hostname for mdns (psychic.local) // hostname for mdns (psychic.local)
const char *local_hostname = "psychic"; const char* local_hostname = "psychic";
PsychicHttpServer server; PsychicHttpServer server;
PsychicWebSocketHandler websocketHandler; PsychicWebSocketHandler websocketHandler;
typedef struct { typedef struct
int socket; {
char *buffer; int socket;
size_t len; char* buffer;
size_t len;
} WebsocketMessage; } WebsocketMessage;
QueueHandle_t wsMessages; QueueHandle_t wsMessages;
@@ -50,7 +51,7 @@ bool connectToWifi()
Serial.print("[WiFi] Connecting to "); Serial.print("[WiFi] Connecting to ");
Serial.println(ssid); Serial.println(ssid);
//setup our wifi // setup our wifi
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
@@ -113,7 +114,7 @@ void setup()
Serial.begin(115200); Serial.begin(115200);
delay(10); delay(10);
//prepare our message queue of 10 messages // prepare our message queue of 10 messages
wsMessages = xQueueCreate(10, sizeof(WebsocketMessage)); wsMessages = xQueueCreate(10, sizeof(WebsocketMessage));
if (wsMessages == 0) if (wsMessages == 0)
Serial.printf("Failed to create queue= %p\n", wsMessages); Serial.printf("Failed to create queue= %p\n", wsMessages);
@@ -122,34 +123,33 @@ void setup()
// To debug, please enable Core Debug Level to Verbose // To debug, please enable Core Debug Level to Verbose
if (connectToWifi()) if (connectToWifi())
{ {
//set up our esp32 to listen on the local_hostname.local domain // set up our esp32 to listen on the local_hostname.local domain
if (!MDNS.begin(local_hostname)) { if (!MDNS.begin(local_hostname))
{
Serial.println("Error starting mDNS"); Serial.println("Error starting mDNS");
return; return;
} }
MDNS.addService("http", "tcp", 80); MDNS.addService("http", "tcp", 80);
if(!LittleFS.begin()) if (!LittleFS.begin())
{ {
Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode");
return; return;
} }
server.listen(80); // this is where our /index.html file lives
// curl -i http://psychic.local/
server.serveStatic("/", LittleFS, "/www/");
//this is where our /index.html file lives // a websocket echo server
// curl -i http://psychic.local/ // npm install -g wscat
PsychicStaticFileHandler* handler = server.serveStatic("/", LittleFS, "/www/"); // wscat -c ws://psychic.local/ws
websocketHandler.onOpen([](PsychicWebSocketClient* client)
//a websocket echo server {
// npm install -g wscat Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str());
// wscat -c ws://psychic.local/ws client->sendMessage("Hello!"); });
websocketHandler.onOpen([](PsychicWebSocketClient *client) { websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame)
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString()); {
client->sendMessage("Hello!");
});
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame)
{
Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload);
//we are allocating memory here, and the worker will free it //we are allocating memory here, and the worker will free it
@@ -181,11 +181,9 @@ void setup()
if (!uxQueueSpacesAvailable(wsMessages)) if (!uxQueueSpacesAvailable(wsMessages))
return request->reply("Queue Full"); return request->reply("Queue Full");
return ESP_OK; return ESP_OK; });
}); websocketHandler.onClose([](PsychicWebSocketClient* client)
websocketHandler.onClose([](PsychicWebSocketClient *client) { { Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str()); });
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
});
server.on("/ws", &websocketHandler); server.on("/ws", &websocketHandler);
} }
} }
@@ -195,29 +193,30 @@ char output[60];
void loop() void loop()
{ {
//process our websockets outside the callback. // process our websockets outside the callback.
WebsocketMessage message; WebsocketMessage message;
while (xQueueReceive(wsMessages, &message, 0) == pdTRUE) while (xQueueReceive(wsMessages, &message, 0) == pdTRUE)
{ {
//make sure our client is still good. // make sure our client is still good.
PsychicWebSocketClient *client = websocketHandler.getClient(message.socket); PsychicWebSocketClient* client = websocketHandler.getClient(message.socket);
if (client == NULL) { if (client == NULL)
{
Serial.printf("[socket] client #%d bad, bailing\n", message.socket); Serial.printf("[socket] client #%d bad, bailing\n", message.socket);
return; return;
} }
//echo it back to the client. // echo it back to the client.
//alternatively, this is where you would deserialize a json message, parse it, and generate a response if needed // alternatively, this is where you would deserialize a json message, parse it, and generate a response if needed
client->sendMessage(HTTPD_WS_TYPE_TEXT, message.buffer, message.len); client->sendMessage(HTTPD_WS_TYPE_TEXT, message.buffer, message.len);
//make sure to release our memory! // make sure to release our memory!
free(message.buffer); free(message.buffer);
} }
//send a periodic update to all clients // send a periodic update to all clients
if (millis() - lastUpdate > 2000) if (millis() - lastUpdate > 2000)
{ {
sprintf(output, "Millis: %d\n", millis()); sprintf(output, "Millis: %lu\n", millis());
websocketHandler.sendAll(output); websocketHandler.sendAll(output);
lastUpdate = millis(); lastUpdate = millis();

View File

@@ -1,15 +1,13 @@
{ {
"name": "PsychicHttp", "name": "PsychicHttp",
"version": "1.2.1", "version": "2.0.0",
"description": "Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266", "description": "Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266",
"keywords": "network,http,https,tcp,ssl,tls,websocket,espasyncwebserver", "keywords": "network,http,https,tcp,ssl,tls,websocket,espasyncwebserver",
"repository": "repository": {
{
"type": "git", "type": "git",
"url": "https://github.com/hoeken/PsychicHttp" "url": "https://github.com/hoeken/PsychicHttp"
}, },
"authors": "authors": [
[
{ {
"name": "Zach Hoeken", "name": "Zach Hoeken",
"email": "hoeken@gmail.com", "email": "hoeken@gmail.com",
@@ -36,8 +34,8 @@
}, },
{ {
"owner": "plageoj", "owner": "plageoj",
"name" : "UrlEncode", "name": "UrlEncode",
"version" : "^1.0.1" "version": "^1.0.1"
} }
], ],
"export": { "export": {
@@ -50,4 +48,4 @@
"README.md" "README.md"
] ]
} }
} }

View File

@@ -1,5 +1,5 @@
name=PsychicHttp name=PsychicHttp
version=1.2.1 version=2.0.0
author=Zach Hoeken <hoeken@gmail.com> author=Zach Hoeken <hoeken@gmail.com>
maintainer=Zach Hoeken <hoeken@gmail.com> maintainer=Zach Hoeken <hoeken@gmail.com>
sentence=PsychicHttp is a robust webserver that supports http/https + websockets. sentence=PsychicHttp is a robust webserver that supports http/https + websockets.

View File

@@ -0,0 +1,30 @@
# PsychicHandler
- [x] create addMiddleware()
- [x] create runMiddleware()
- [ ] move all the handler::canHandle() stuff into filter();
- [ ] canHandle should be declared static
# PsychicEndpoint
- [ ] convert setAuthentication() to add AuthMiddleware instead.
## PsychicHttpServer
- [ ] add _chain
- [ ] create addMiddleware()
- [ ] create runMiddleware()
- [ ] create removeMiddleware(name)
- [ ] _filters -> _middleware
- [ ] destructor / cleanup
# PsychicRequest
- [ ] add _response pointer to PsychicRequest, created in constructor
- [ ] request->beginReply() should return existing _response pointer
- [ ] requestAuthentication() -> should move to response?
# PsychicResponse
- how do we have extended classes when we have a pre-declared base PsychicResponse object?
- the delegation style is really ugly and causes problems with inheritance

View File

@@ -0,0 +1,7 @@
# Name ,Type ,SubType ,Offset ,Size ,Flags
nvs ,data ,nvs ,36K ,20K ,
otadata ,data ,ota ,56K ,8K ,
app0 ,app ,ota_0 ,64K ,1856K ,
app1 ,app ,ota_1 ,1920K ,1856K ,
spiffs ,data ,spiffs ,3776K ,256K ,
coredump ,data ,coredump ,4032K ,64K ,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 36K 20K
3 otadata data ota 56K 8K
4 app0 app ota_0 64K 1856K
5 app1 app ota_1 1920K 1856K
6 spiffs data spiffs 3776K 256K
7 coredump data coredump 4032K 64K

View File

@@ -0,0 +1,108 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
lib_dir = .
src_dir = examples/platformio/src
data_dir = examples/platformio/data
; src_dir = examples/websockets/src
; data_dir = examples/websockets/data
; src_dir = examples/arduino
; data_dir = examples/arduino/data
; src_dir = examples/arduino/arduino_captive_portal
; data_dir = examples/arduino/arduino_captive_portal/data
; src_dir = examples/arduino/arduino_ota
; data_dir = examples/arduino/arduino_ota/data
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
board_build.partitions = partitions-4MB.csv
board_build.filesystem = littlefs
lib_deps =
bblanchon/ArduinoJson
plageoj/UrlEncode
lib_ignore =
examples
build_flags =
-Wall
-Wextra
-Og
-D CONFIG_ARDUHAL_LOG_COLORS
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
[env:arduino2]
platform = espressif32@6.8.1
[env:arduino2-ssl]
platform = espressif32@6.8.1
build_flags = -DPSY_ENABLE_SSL
[env:arduino2-regex]
platform = espressif32@6.8.1
build_flags = -DPSY_ENABLE_REGEX
[env:arduino3]
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip
[env:arduino3-ssl]
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip
build_flags = -DPSY_ENABLE_SSL
[env:arduino3-regex]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
build_flags = -D PSY_ENABLE_REGEX
; [env:waveshare-4-3-touchscreen]
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
; lib_deps = ${env.lib_deps}
; https://github.com/esp-arduino-libs/ESP32_IO_Expander
; build_flags =
; -D PSY_ENABLE_SDCARD
; -D WAVESHARE_43_TOUCH
[env:pioarduino-c6]
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip
board = esp32-c6-devkitc-1
[env:mathieu]
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip
board = esp32dev
lib_deps = ${env.lib_deps}
; bblanchon/ArduinoTrace@^1.2.0
build_flags = ${env.build_flags}
; -D PSY_DEVMODE
; -D PSY_ENABLE_REGEX
-D PSY_ENABLE_SSL
[env:hoeken]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc1/platform-espressif32.zip
board = esp32-s3-devkitc-1
lib_deps = ${env.lib_deps}
bblanchon/ArduinoTrace@^1.2.0
build_flags = ${env.build_flags}
-D PSY_DEVMODE
[env:ci]
platform = ${sysenv.PIO_PLATFORM}
board = ${sysenv.PIO_BOARD}

View File

@@ -1,12 +1,12 @@
#include "ChunkPrinter.h" #include "ChunkPrinter.h"
ChunkPrinter::ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len) : ChunkPrinter::ChunkPrinter(PsychicResponse* response, uint8_t* buffer, size_t len) : _response(response),
_response(response), _buffer(buffer),
_buffer(buffer), _length(len),
_length(len), _pos(0)
_pos(0) {
{} }
ChunkPrinter::~ChunkPrinter() ChunkPrinter::~ChunkPrinter()
{ {
@@ -16,34 +16,34 @@ ChunkPrinter::~ChunkPrinter()
size_t ChunkPrinter::write(uint8_t c) size_t ChunkPrinter::write(uint8_t c)
{ {
esp_err_t err; esp_err_t err;
//if we're full, send a chunk // if we're full, send a chunk
if (_pos == _length) if (_pos == _length)
{ {
_pos = 0; _pos = 0;
err = _response->sendChunk(_buffer, _length); err = _response->sendChunk(_buffer, _length);
if (err != ESP_OK) if (err != ESP_OK)
return 0; return 0;
} }
_buffer[_pos] = c; _buffer[_pos] = c;
_pos++; _pos++;
return 1; return 1;
} }
size_t ChunkPrinter::write(const uint8_t *buffer, size_t size) size_t ChunkPrinter::write(const uint8_t* buffer, size_t size)
{ {
size_t written = 0; size_t written = 0;
while (written < size) while (written < size)
{ {
size_t space = _length - _pos; size_t space = _length - _pos;
size_t blockSize = std::min(space, size - written); size_t blockSize = std::min(space, size - written);
memcpy(_buffer + _pos, buffer + written, blockSize); memcpy(_buffer + _pos, buffer + written, blockSize);
_pos += blockSize; _pos += blockSize;
if (_pos == _length) if (_pos == _length)
{ {
_pos = 0; _pos = 0;
@@ -51,7 +51,7 @@ size_t ChunkPrinter::write(const uint8_t *buffer, size_t size)
if (_response->sendChunk(_buffer, _length) != ESP_OK) if (_response->sendChunk(_buffer, _length) != ESP_OK)
return written; return written;
} }
written += blockSize; //Update if sent correctly. written += blockSize; // Update if sent correctly.
} }
return written; return written;
} }
@@ -65,18 +65,19 @@ void ChunkPrinter::flush()
} }
} }
size_t ChunkPrinter::copyFrom(Stream &stream) size_t ChunkPrinter::copyFrom(Stream& stream)
{ {
size_t count = 0; size_t count = 0;
while (stream.available()){ while (stream.available())
{
if (_pos == _length) if (_pos == _length)
{ {
_response->sendChunk(_buffer, _length); _response->sendChunk(_buffer, _length);
_pos = 0; _pos = 0;
} }
size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos); size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos);
_pos += readBytes; _pos += readBytes;
count += readBytes; count += readBytes;

View File

@@ -7,19 +7,19 @@
class ChunkPrinter : public Print class ChunkPrinter : public Print
{ {
private: private:
PsychicResponse *_response; PsychicResponse* _response;
uint8_t *_buffer; uint8_t* _buffer;
size_t _length; size_t _length;
size_t _pos; size_t _pos;
public: public:
ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len); ChunkPrinter(PsychicResponse* response, uint8_t* buffer, size_t len);
~ChunkPrinter(); ~ChunkPrinter();
size_t write(uint8_t c) override;
size_t write(const uint8_t *buffer, size_t size) override;
size_t copyFrom(Stream &stream); size_t write(uint8_t c) override;
size_t write(const uint8_t* buffer, size_t size) override;
size_t copyFrom(Stream& stream);
void flush() override; void flush() override;
}; };

View File

@@ -0,0 +1,426 @@
#include "MultipartProcessor.h"
#include "PsychicRequest.h"
enum
{
EXPECT_BOUNDARY,
PARSE_HEADERS,
WAIT_FOR_RETURN1,
EXPECT_FEED1,
EXPECT_DASH1,
EXPECT_DASH2,
BOUNDARY_OR_DATA,
DASH3_OR_RETURN2,
EXPECT_FEED2,
PARSING_FINISHED,
PARSE_ERROR
};
MultipartProcessor::MultipartProcessor(PsychicRequest* request, PsychicUploadCallback uploadCallback) : _request(request),
_uploadCallback(uploadCallback),
_temp(),
_parsedLength(0),
_multiParseState(EXPECT_BOUNDARY),
_boundaryPosition(0),
_itemStartIndex(0),
_itemSize(0),
_itemName(),
_itemFilename(),
_itemType(),
_itemValue(),
_itemBuffer(0),
_itemBufferIndex(0),
_itemIsFile(false)
{
}
MultipartProcessor::~MultipartProcessor() {}
esp_err_t MultipartProcessor::process()
{
esp_err_t err = ESP_OK;
_parsedLength = 0;
String value = _request->header("Content-Type");
if (value.startsWith("multipart/"))
{
_boundary = value.substring(value.indexOf('=') + 1);
_boundary.replace("\"", "");
}
else
{
ESP_LOGE(PH_TAG, "No multipart boundary found.");
return ESP_ERR_HTTPD_INVALID_REQ;
}
char* buf = (char*)malloc(FILE_CHUNK_SIZE);
int received;
unsigned long index = 0;
/* Content length of the request gives the size of the file being uploaded */
int remaining = _request->contentLength();
while (remaining > 0)
{
#ifdef ENABLE_ASYNC
httpd_sess_update_lru_counter(request->server()->server, request->client()->socket());
#endif
/* Receive the file part by part into a buffer */
if ((received = httpd_req_recv(_request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0)
{
/* Retry if timeout occurred */
if (received == HTTPD_SOCK_ERR_TIMEOUT)
continue;
// bail if we got an error
else if (received == HTTPD_SOCK_ERR_FAIL)
{
ESP_LOGE(PH_TAG, "Socket error");
err = ESP_FAIL;
break;
}
}
// parse it 1 byte at a time.
for (int i = 0; i < received; i++)
{
/* Keep track of remaining size of the file left to be uploaded */
remaining--;
index++;
// send it to our parser
_parseMultipartPostByte(buf[i], !remaining);
_parsedLength++;
}
}
// dont forget to free our buffer
free(buf);
return err;
}
esp_err_t MultipartProcessor::process(const char* body)
{
esp_err_t err = ESP_OK;
_parsedLength = 0;
String value = _request->header("Content-Type");
if (value.startsWith("multipart/"))
{
_boundary = value.substring(value.indexOf('=') + 1);
_boundary.replace("\"", "");
}
else
{
ESP_LOGE(PH_TAG, "No multipart boundary found.");
return ESP_ERR_HTTPD_INVALID_REQ;
}
// loop over the whole string
unsigned int size = strlen(body);
for (unsigned i = 0; i < size; i++)
{
// send it to our parser
_parseMultipartPostByte(body[i], i == size - 1);
_parsedLength++;
}
return err;
}
void MultipartProcessor::_handleUploadByte(uint8_t data, bool last)
{
_itemBuffer[_itemBufferIndex++] = data;
if (last || _itemBufferIndex == FILE_CHUNK_SIZE)
{
if (_uploadCallback)
_uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, last);
_itemBufferIndex = 0;
}
}
#define itemWriteByte(b) \
do \
{ \
_itemSize++; \
if (_itemIsFile) \
_handleUploadByte(b, last); \
else \
_itemValue += (char)(b); \
} while (0)
void MultipartProcessor::_parseMultipartPostByte(uint8_t data, bool last)
{
if (_multiParseState == PARSE_ERROR)
{
// not sure we can end up with an error during buffer fill, but jsut to be safe
if (_itemBuffer != NULL)
{
free(_itemBuffer);
_itemBuffer = NULL;
}
return;
}
if (!_parsedLength)
{
_multiParseState = EXPECT_BOUNDARY;
_temp = String();
_itemName = String();
_itemFilename = String();
_itemType = String();
}
if (_multiParseState == WAIT_FOR_RETURN1)
{
if (data != '\r')
{
itemWriteByte(data);
}
else
{
_multiParseState = EXPECT_FEED1;
}
}
else if (_multiParseState == EXPECT_BOUNDARY)
{
if (_parsedLength < 2 && data != '-')
{
ESP_LOGE(PH_TAG, "Multipart: No boundary");
_multiParseState = PARSE_ERROR;
return;
}
else if (_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data)
{
ESP_LOGE(PH_TAG, "Multipart: Multipart malformed");
_multiParseState = PARSE_ERROR;
return;
}
else if (_parsedLength - 2 == _boundary.length() && data != '\r')
{
ESP_LOGE(PH_TAG, "Multipart: Multipart missing carriage return");
_multiParseState = PARSE_ERROR;
return;
}
else if (_parsedLength - 3 == _boundary.length())
{
if (data != '\n')
{
ESP_LOGE(PH_TAG, "Multipart: Multipart missing newline");
_multiParseState = PARSE_ERROR;
return;
}
_multiParseState = PARSE_HEADERS;
_itemIsFile = false;
}
}
else if (_multiParseState == PARSE_HEADERS)
{
if ((char)data != '\r' && (char)data != '\n')
_temp += (char)data;
if ((char)data == '\n')
{
if (_temp.length())
{
if (_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase("Content-Type"))
{
_itemType = _temp.substring(14);
_itemIsFile = true;
}
else if (_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase("Content-Disposition"))
{
_temp = _temp.substring(_temp.indexOf(';') + 2);
while (_temp.indexOf(';') > 0)
{
String name = _temp.substring(0, _temp.indexOf('='));
String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1);
if (name == "name")
{
_itemName = nameVal;
}
else if (name == "filename")
{
_itemFilename = nameVal;
_itemIsFile = true;
}
_temp = _temp.substring(_temp.indexOf(';') + 2);
}
String name = _temp.substring(0, _temp.indexOf('='));
String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1);
if (name == "name")
{
_itemName = nameVal;
}
else if (name == "filename")
{
_itemFilename = nameVal;
_itemIsFile = true;
}
}
_temp = String();
}
else
{
_multiParseState = WAIT_FOR_RETURN1;
// value starts from here
_itemSize = 0;
_itemStartIndex = _parsedLength;
_itemValue = String();
if (_itemIsFile)
{
if (_itemBuffer)
free(_itemBuffer);
_itemBuffer = (uint8_t*)malloc(FILE_CHUNK_SIZE);
if (_itemBuffer == NULL)
{
ESP_LOGE(PH_TAG, "Multipart: Failed to allocate buffer");
_multiParseState = PARSE_ERROR;
return;
}
_itemBufferIndex = 0;
}
}
}
}
else if (_multiParseState == EXPECT_FEED1)
{
if (data != '\n')
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
_parseMultipartPostByte(data, last);
}
else
{
_multiParseState = EXPECT_DASH1;
}
}
else if (_multiParseState == EXPECT_DASH1)
{
if (data != '-')
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
itemWriteByte('\n');
_parseMultipartPostByte(data, last);
}
else
{
_multiParseState = EXPECT_DASH2;
}
}
else if (_multiParseState == EXPECT_DASH2)
{
if (data != '-')
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
itemWriteByte('\n');
itemWriteByte('-');
_parseMultipartPostByte(data, last);
}
else
{
_multiParseState = BOUNDARY_OR_DATA;
_boundaryPosition = 0;
}
}
else if (_multiParseState == BOUNDARY_OR_DATA)
{
if (_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data)
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
itemWriteByte('\n');
itemWriteByte('-');
itemWriteByte('-');
uint8_t i;
for (i = 0; i < _boundaryPosition; i++)
itemWriteByte(_boundary.c_str()[i]);
_parseMultipartPostByte(data, last);
}
else if (_boundaryPosition == _boundary.length() - 1)
{
_multiParseState = DASH3_OR_RETURN2;
if (!_itemIsFile)
{
// External - Add parameter!
_request->addParam(_itemName, _itemValue);
}
else
{
if (_itemSize)
{
if (_uploadCallback)
{
_uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);
}
_itemBufferIndex = 0;
// External - Add parameter!
_request->addParam(new PsychicWebParameter(_itemName, _itemFilename, true, true, _itemSize));
}
free(_itemBuffer);
_itemBuffer = NULL;
}
}
else
{
_boundaryPosition++;
}
}
else if (_multiParseState == DASH3_OR_RETURN2)
{
if (data == '-' && (_request->contentLength() - _parsedLength - 4) != 0)
{
ESP_LOGE(PH_TAG, "ERROR: The parser got to the end of the POST but is expecting more bytes!");
_multiParseState = PARSE_ERROR;
return;
}
if (data == '\r')
{
_multiParseState = EXPECT_FEED2;
}
else if (data == '-' && _request->contentLength() == (_parsedLength + 4))
{
_multiParseState = PARSING_FINISHED;
}
else
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
itemWriteByte('\n');
itemWriteByte('-');
itemWriteByte('-');
uint8_t i;
for (i = 0; i < _boundary.length(); i++)
itemWriteByte(_boundary.c_str()[i]);
_parseMultipartPostByte(data, last);
}
}
else if (_multiParseState == EXPECT_FEED2)
{
if (data == '\n')
{
_multiParseState = PARSE_HEADERS;
_itemIsFile = false;
}
else
{
_multiParseState = WAIT_FOR_RETURN1;
itemWriteByte('\r');
itemWriteByte('\n');
itemWriteByte('-');
itemWriteByte('-');
uint8_t i;
for (i = 0; i < _boundary.length(); i++)
itemWriteByte(_boundary.c_str()[i]);
itemWriteByte('\r');
_parseMultipartPostByte(data, last);
}
}
}

View File

@@ -0,0 +1,42 @@
#ifndef MULTIPART_PROCESSOR_H
#define MULTIPART_PROCESSOR_H
#include "PsychicCore.h"
/*
* MultipartProcessor - handle parsing and processing a multipart form.
* */
class MultipartProcessor
{
protected:
PsychicRequest* _request;
PsychicUploadCallback _uploadCallback;
String _temp;
size_t _parsedLength;
uint8_t _multiParseState;
String _boundary;
uint8_t _boundaryPosition;
size_t _itemStartIndex;
size_t _itemSize;
String _itemName;
String _itemFilename;
String _itemType;
String _itemValue;
uint8_t* _itemBuffer;
size_t _itemBufferIndex;
bool _itemIsFile;
void _handleUploadByte(uint8_t data, bool last);
void _parseMultipartPostByte(uint8_t data, bool last);
public:
MultipartProcessor(PsychicRequest* request, PsychicUploadCallback uploadCallback = nullptr);
~MultipartProcessor();
esp_err_t process();
esp_err_t process(const char* body);
};
#endif

View File

@@ -2,21 +2,24 @@
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
#include <lwip/sockets.h> #include <lwip/sockets.h>
PsychicClient::PsychicClient(httpd_handle_t server, int socket) : PsychicClient::PsychicClient(httpd_handle_t server, int socket) : _server(server),
_server(server), _socket(socket),
_socket(socket), _friend(NULL),
_friend(NULL), isNew(false)
isNew(false) {
{}
PsychicClient::~PsychicClient() {
} }
httpd_handle_t PsychicClient::server() { PsychicClient::~PsychicClient()
{
}
httpd_handle_t PsychicClient::server()
{
return _server; return _server;
} }
int PsychicClient::socket() { int PsychicClient::socket()
{
return _socket; return _socket;
} }
@@ -24,49 +27,67 @@ int PsychicClient::socket() {
esp_err_t PsychicClient::close() esp_err_t PsychicClient::close()
{ {
esp_err_t err = httpd_sess_trigger_close(_server, _socket); esp_err_t err = httpd_sess_trigger_close(_server, _socket);
//PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list. // PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list.
return err; return err;
} }
IPAddress PsychicClient::localIP() IPAddress PsychicClient::localIP()
{ {
IPAddress address(0,0,0,0); IPAddress address(0, 0, 0, 0);
char ipstr[INET6_ADDRSTRLEN]; char ipstr[INET6_ADDRSTRLEN];
struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
socklen_t addr_size = sizeof(addr); socklen_t addr_size = sizeof(addr);
if (getsockname(_socket, (struct sockaddr *)&addr, &addr_size) < 0) { if (getsockname(_socket, (struct sockaddr*)&addr, &addr_size) < 0) {
ESP_LOGE(PH_TAG, "Error getting client IP"); ESP_LOGE(PH_TAG, "Error getting client IP");
return address; return address;
} }
// Convert to IPv4 string // Convert to IPv4 string
inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));
//ESP_LOGD(PH_TAG, "Client Local IP => %s", ipstr); // ESP_LOGD(PH_TAG, "Client Local IP => %s", ipstr);
address.fromString(ipstr); address.fromString(ipstr);
return address; return address;
} }
uint16_t PsychicClient::localPort() const
{
struct sockaddr_storage addr;
socklen_t len = sizeof addr;
getsockname(_socket, (struct sockaddr*)&addr, &len);
struct sockaddr_in* s = (struct sockaddr_in*)&addr;
return ntohs(s->sin_port);
}
IPAddress PsychicClient::remoteIP() IPAddress PsychicClient::remoteIP()
{ {
IPAddress address(0,0,0,0); IPAddress address(0, 0, 0, 0);
char ipstr[INET6_ADDRSTRLEN]; char ipstr[INET6_ADDRSTRLEN];
struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
socklen_t addr_size = sizeof(addr); socklen_t addr_size = sizeof(addr);
if (getpeername(_socket, (struct sockaddr *)&addr, &addr_size) < 0) { if (getpeername(_socket, (struct sockaddr*)&addr, &addr_size) < 0) {
ESP_LOGE(PH_TAG, "Error getting client IP"); ESP_LOGE(PH_TAG, "Error getting client IP");
return address; return address;
} }
// Convert to IPv4 string // Convert to IPv4 string
inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));
//ESP_LOGD(PH_TAG, "Client Remote IP => %s", ipstr); // ESP_LOGD(PH_TAG, "Client Remote IP => %s", ipstr);
address.fromString(ipstr); address.fromString(ipstr);
return address; return address;
} }
uint16_t PsychicClient::remotePort() const
{
struct sockaddr_storage addr;
socklen_t len = sizeof addr;
getpeername(_socket, (struct sockaddr*)&addr, &len);
struct sockaddr_in* s = (struct sockaddr_in*)&addr;
return ntohs(s->sin_port);
}

View File

@@ -4,10 +4,12 @@
#include "PsychicCore.h" #include "PsychicCore.h"
/* /*
* PsychicClient :: Generic wrapper around the ESP-IDF socket * PsychicClient :: Generic wrapper around the ESP-IDF socket
*/ */
class PsychicClient
{
class PsychicClient {
protected: protected:
httpd_handle_t _server; httpd_handle_t _server;
int _socket; int _socket;
@@ -16,9 +18,9 @@ class PsychicClient {
PsychicClient(httpd_handle_t server, int socket); PsychicClient(httpd_handle_t server, int socket);
~PsychicClient(); ~PsychicClient();
//no idea if this is the right way to do it or not, but lets see. // no idea if this is the right way to do it or not, but lets see.
//pointer to our derived class (eg. PsychicWebSocketConnection) // pointer to our derived class (eg. PsychicWebSocketConnection)
void *_friend; void* _friend;
bool isNew = false; bool isNew = false;
@@ -29,7 +31,9 @@ class PsychicClient {
esp_err_t close(); esp_err_t close();
IPAddress localIP(); IPAddress localIP();
uint16_t localPort() const;
IPAddress remoteIP(); IPAddress remoteIP();
uint16_t remotePort() const;
}; };
#endif #endif

View File

@@ -3,17 +3,8 @@
#define PH_TAG "psychic" #define PH_TAG "psychic"
//version numbers
#define PSYCHIC_HTTP_VERSION_MAJOR 1
#define PSYCHIC_HTTP_VERSION_MINOR 1
#define PSYCHIC_HTTP_VERSION_PATCH 0
#ifndef MAX_COOKIE_SIZE
#define MAX_COOKIE_SIZE 512
#endif
#ifndef FILE_CHUNK_SIZE #ifndef FILE_CHUNK_SIZE
#define FILE_CHUNK_SIZE 8*1024 #define FILE_CHUNK_SIZE 8 * 1024
#endif #endif
#ifndef STREAM_CHUNK_SIZE #ifndef STREAM_CHUNK_SIZE
@@ -21,87 +12,93 @@
#endif #endif
#ifndef MAX_UPLOAD_SIZE #ifndef MAX_UPLOAD_SIZE
#define MAX_UPLOAD_SIZE (2048*1024) // 2MB #define MAX_UPLOAD_SIZE (2048 * 1024) // 2MB
#endif #endif
#ifndef MAX_REQUEST_BODY_SIZE #ifndef MAX_REQUEST_BODY_SIZE
#define MAX_REQUEST_BODY_SIZE (16*1024) //16K #define MAX_REQUEST_BODY_SIZE (16 * 1024) // 16K
#endif #endif
#ifdef ARDUINO #ifdef ARDUINO
#include <Arduino.h> #include <Arduino.h>
#endif #endif
#include <esp_http_server.h>
#include <map>
#include <list>
#include <libb64/cencode.h>
#include "esp_random.h"
#include "MD5Builder.h"
#include <UrlEncode.h>
#include "FS.h" #include "FS.h"
#include "MD5Builder.h"
#include "esp_random.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <UrlEncode.h>
#include <esp_http_server.h>
#include <libb64/cencode.h>
#include <list>
#include <map>
enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; #ifdef PSY_DEVMODE
#include "ArduinoTrace.h"
#endif
enum HTTPAuthMethod {
BASIC_AUTH,
DIGEST_AUTH
};
String urlDecode(const char* encoded); String urlDecode(const char* encoded);
class PsychicHttpServer; class PsychicHttpServer;
class PsychicRequest; class PsychicRequest;
class PsychicResponse;
class PsychicWebSocketRequest; class PsychicWebSocketRequest;
class PsychicClient; class PsychicClient;
//filter function definition // filter function definition
typedef std::function<bool(PsychicRequest *request)> PsychicRequestFilterFunction; typedef std::function<bool(PsychicRequest* request)> PsychicRequestFilterFunction;
//client connect callback // middleware function definition
typedef std::function<void(PsychicClient *client)> PsychicClientCallback; typedef std::function<esp_err_t()> PsychicMiddlewareNext;
typedef std::function<esp_err_t(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)> PsychicMiddlewareCallback;
//callback definitions // client connect callback
typedef std::function<esp_err_t(PsychicRequest *request)> PsychicHttpRequestCallback; typedef std::function<void(PsychicClient* client)> PsychicClientCallback;
typedef std::function<esp_err_t(PsychicRequest *request, JsonVariant &json)> PsychicJsonRequestCallback;
// callback definitions
typedef std::function<esp_err_t(PsychicRequest* request, PsychicResponse* response)> PsychicHttpRequestCallback;
typedef std::function<esp_err_t(PsychicRequest* request, PsychicResponse* response, JsonVariant& json)> PsychicJsonRequestCallback;
typedef std::function<esp_err_t(PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool final)> PsychicUploadCallback;
struct HTTPHeader { struct HTTPHeader {
char * field; String field;
char * value; String value;
}; };
class DefaultHeaders { class DefaultHeaders
std::list<HTTPHeader> _headers; {
std::list<HTTPHeader> _headers;
public: public:
DefaultHeaders() {} DefaultHeaders() {}
void addHeader(const String& field, const String& value) void addHeader(const String& field, const String& value)
{ {
addHeader(field.c_str(), value.c_str()); _headers.push_back({field, value});
} }
void addHeader(const char * field, const char * value) void addHeader(const char* field, const char* value)
{ {
HTTPHeader header; _headers.push_back({field, value});
}
//these are just going to stick around forever. const std::list<HTTPHeader>& getHeaders() { return _headers; }
header.field =(char *)malloc(strlen(field)+1);
header.value = (char *)malloc(strlen(value)+1);
strlcpy(header.field, field, strlen(field)+1); // delete the copy constructor, singleton class
strlcpy(header.value, value, strlen(value)+1); DefaultHeaders(DefaultHeaders const&) = delete;
DefaultHeaders& operator=(DefaultHeaders const&) = delete;
_headers.push_back(header); // single static class interface
} static DefaultHeaders& Instance()
{
const std::list<HTTPHeader>& getHeaders() { return _headers; } static DefaultHeaders instance;
return instance;
//delete the copy constructor, singleton class }
DefaultHeaders(DefaultHeaders const &) = delete;
DefaultHeaders &operator=(DefaultHeaders const &) = delete;
//single static class interface
static DefaultHeaders &Instance() {
static DefaultHeaders instance;
return instance;
}
}; };
#endif //PsychicCore_h #endif // PsychicCore_h

View File

@@ -1,90 +1,140 @@
#include "PsychicEndpoint.h" #include "PsychicEndpoint.h"
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
PsychicEndpoint::PsychicEndpoint() : PsychicEndpoint::PsychicEndpoint() : _server(NULL),
_server(NULL), _uri(""),
_uri(""), _method(HTTP_GET),
_method(HTTP_GET), _handler(NULL)
_handler(NULL)
{ {
} }
PsychicEndpoint::PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri) : PsychicEndpoint::PsychicEndpoint(PsychicHttpServer* server, int method, const char* uri) : _server(server),
_server(server), _uri(uri),
_uri(uri), _method(method),
_method(method), _handler(NULL)
_handler(NULL)
{ {
} }
PsychicEndpoint * PsychicEndpoint::setHandler(PsychicHandler *handler) PsychicEndpoint* PsychicEndpoint::setHandler(PsychicHandler* handler)
{ {
//clean up old / default handler // clean up old / default handler
if (_handler != NULL) if (_handler != NULL)
delete _handler; delete _handler;
//get our new pointer // get our new pointer
_handler = handler; _handler = handler;
//keep a pointer to the server // keep a pointer to the server
_handler->_server = _server; _handler->_server = _server;
return this; return this;
} }
PsychicHandler * PsychicEndpoint::handler() PsychicHandler* PsychicEndpoint::handler()
{ {
return _handler; return _handler;
} }
String PsychicEndpoint::uri() { String PsychicEndpoint::uri()
{
return _uri; return _uri;
} }
esp_err_t PsychicEndpoint::requestCallback(httpd_req_t *req) esp_err_t PsychicEndpoint::requestCallback(httpd_req_t* req)
{ {
#ifdef ENABLE_ASYNC #ifdef ENABLE_ASYNC
if (is_on_async_worker_thread() == false) { if (is_on_async_worker_thread() == false) {
if (submit_async_req(req, PsychicEndpoint::requestCallback) == ESP_OK) { if (submit_async_req(req, PsychicEndpoint::requestCallback) == ESP_OK) {
return ESP_OK; return ESP_OK;
} else { } else {
httpd_resp_set_status(req, "503 Busy"); httpd_resp_set_status(req, "503 Busy");
httpd_resp_sendstr(req, "No workers available. Server busy.</div>"); httpd_resp_sendstr(req, "No workers available. Server busy.</div>");
return ESP_OK; return ESP_OK;
}
} }
#endif }
#endif
PsychicEndpoint *self = (PsychicEndpoint *)req->user_ctx; PsychicEndpoint* self = (PsychicEndpoint*)req->user_ctx;
PsychicHandler *handler = self->handler();
PsychicRequest request(self->_server, req); PsychicRequest request(self->_server, req);
//make sure we have a handler esp_err_t err = self->process(&request);
if (handler != NULL)
{
if (handler->filter(&request) && handler->canHandle(&request))
{
//check our credentials
if (handler->needsAuthentication(&request))
return handler->authenticate(&request);
//pass it to our handler if (err == HTTPD_404_NOT_FOUND)
return handler->handleRequest(&request); return PsychicHttpServer::requestHandler(req);
}
//pass it to our generic handlers if (err == ESP_ERR_HTTPD_INVALID_REQ)
else return request.response()->error(HTTPD_500_INTERNAL_SERVER_ERROR, "No handler registered.");
return PsychicHttpServer::notFoundHandler(req, HTTPD_500_INTERNAL_SERVER_ERROR);
} return err;
}
bool PsychicEndpoint::matches(const char* uri)
{
// we only want to match the path, no GET strings
char* ptr;
size_t position = 0;
// look for a ? and set our path length to that,
ptr = strchr(uri, '?');
if (ptr != NULL)
position = (size_t)(int)(ptr - uri);
// or use the whole uri if not found
else else
return request.reply(500, "text/html", "No handler registered."); position = strlen(uri);
// do we have a per-endpoint match function
if (this->getURIMatchFunction() != NULL) {
// ESP_LOGD(PH_TAG, "Match? %s == %s (%d)", _uri.c_str(), uri, position);
return this->getURIMatchFunction()(_uri.c_str(), uri, (size_t)position);
}
// do we have a global match function
if (_server->getURIMatchFunction() != NULL) {
// ESP_LOGD(PH_TAG, "Match? %s == %s (%d)", _uri.c_str(), uri, position);
return _server->getURIMatchFunction()(_uri.c_str(), uri, (size_t)position);
} else {
ESP_LOGE(PH_TAG, "No uri matching function set");
return false;
}
} }
PsychicEndpoint* PsychicEndpoint::setFilter(PsychicRequestFilterFunction fn) { httpd_uri_match_func_t PsychicEndpoint::getURIMatchFunction()
_handler->setFilter(fn); {
return _uri_match_fn;
}
void PsychicEndpoint::setURIMatchFunction(httpd_uri_match_func_t match_fn)
{
_uri_match_fn = match_fn;
}
PsychicEndpoint* PsychicEndpoint::addFilter(PsychicRequestFilterFunction fn)
{
_handler->addFilter(fn);
return this; return this;
} }
PsychicEndpoint* PsychicEndpoint::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) { PsychicEndpoint* PsychicEndpoint::addMiddleware(PsychicMiddleware* middleware)
_handler->setAuthentication(username, password, method, realm, authFailMsg); {
_handler->addMiddleware(middleware);
return this; return this;
}; }
PsychicEndpoint* PsychicEndpoint::addMiddleware(PsychicMiddlewareCallback fn)
{
_handler->addMiddleware(fn);
return this;
}
void PsychicEndpoint::removeMiddleware(PsychicMiddleware* middleware)
{
_handler->removeMiddleware(middleware);
}
esp_err_t PsychicEndpoint::process(PsychicRequest* request)
{
esp_err_t ret = ESP_ERR_HTTPD_INVALID_REQ;
if (_handler != NULL)
ret = _handler->process(request);
ESP_LOGD(PH_TAG, "Endpoint %s processed %s: %s", _uri.c_str(), request->uri().c_str(), esp_err_to_name(ret));
return ret;
}

View File

@@ -4,6 +4,7 @@
#include "PsychicCore.h" #include "PsychicCore.h"
class PsychicHandler; class PsychicHandler;
class PsychicMiddleware;
#ifdef ENABLE_ASYNC #ifdef ENABLE_ASYNC
#include "async_worker.h" #include "async_worker.h"
@@ -11,27 +12,39 @@ class PsychicHandler;
class PsychicEndpoint class PsychicEndpoint
{ {
friend PsychicHttpServer; friend PsychicHttpServer;
private: private:
PsychicHttpServer *_server; PsychicHttpServer* _server;
String _uri; String _uri;
http_method _method; int _method;
PsychicHandler *_handler; PsychicHandler* _handler;
httpd_uri_match_func_t _uri_match_fn = nullptr; // use this change the endpoint matching function.
public: public:
PsychicEndpoint(); PsychicEndpoint();
PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri); PsychicEndpoint(PsychicHttpServer* server, int method, const char* uri);
PsychicEndpoint *setHandler(PsychicHandler *handler); PsychicEndpoint* setHandler(PsychicHandler* handler);
PsychicHandler *handler(); PsychicHandler* handler();
PsychicEndpoint* setFilter(PsychicRequestFilterFunction fn); httpd_uri_match_func_t getURIMatchFunction();
PsychicEndpoint* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = ""); void setURIMatchFunction(httpd_uri_match_func_t match_fn);
bool matches(const char* uri);
// called to process this endpoint with its middleware chain
esp_err_t process(PsychicRequest* request);
PsychicEndpoint* addFilter(PsychicRequestFilterFunction fn);
PsychicEndpoint* addMiddleware(PsychicMiddleware* middleware);
PsychicEndpoint* addMiddleware(PsychicMiddlewareCallback fn);
void removeMiddleware(PsychicMiddleware* middleware);
String uri(); String uri();
static esp_err_t requestCallback(httpd_req_t *req); static esp_err_t requestCallback(httpd_req_t* req);
}; };
#endif // PsychicEndpoint_h #endif // PsychicEndpoint_h

View File

@@ -19,83 +19,88 @@
*/ */
#include "PsychicEventSource.h" #include "PsychicEventSource.h"
#include <string.h>
/*****************************************/ /*****************************************/
// PsychicEventSource - Handler // PsychicEventSource - Handler
/*****************************************/ /*****************************************/
PsychicEventSource::PsychicEventSource() : PsychicEventSource::PsychicEventSource() : PsychicHandler(),
PsychicHandler(), _onOpen(NULL),
_onOpen(NULL), _onClose(NULL)
_onClose(NULL) {
{}
PsychicEventSource::~PsychicEventSource() {
} }
PsychicEventSourceClient * PsychicEventSource::getClient(int socket) PsychicEventSource::~PsychicEventSource()
{ {
PsychicClient *client = PsychicHandler::getClient(socket); }
PsychicEventSourceClient* PsychicEventSource::getClient(int socket)
{
PsychicClient* client = PsychicHandler::getClient(socket);
if (client == NULL) if (client == NULL)
return NULL; return NULL;
return (PsychicEventSourceClient *)client->_friend; return (PsychicEventSourceClient*)client->_friend;
} }
PsychicEventSourceClient * PsychicEventSource::getClient(PsychicClient *client) { PsychicEventSourceClient* PsychicEventSource::getClient(PsychicClient* client)
{
return getClient(client->socket()); return getClient(client->socket());
} }
esp_err_t PsychicEventSource::handleRequest(PsychicRequest *request) esp_err_t PsychicEventSource::handleRequest(PsychicRequest* request, PsychicResponse* resp)
{ {
//start our open ended HTTP response // start our open ended HTTP response
PsychicEventSourceResponse response(request); PsychicEventSourceResponse response(resp);
esp_err_t err = response.send(); esp_err_t err = response.send();
//lookup our client // lookup our client
PsychicClient *client = checkForNewClient(request->client()); PsychicClient* client = checkForNewClient(request->client());
if (client->isNew) if (client->isNew) {
{ // did we get our last id?
//did we get our last id? if (request->hasHeader("Last-Event-ID")) {
if(request->hasHeader("Last-Event-ID")) PsychicEventSourceClient* buddy = getClient(client);
{
PsychicEventSourceClient *buddy = getClient(client);
buddy->_lastId = atoi(request->header("Last-Event-ID").c_str()); buddy->_lastId = atoi(request->header("Last-Event-ID").c_str());
} }
//let our handler know. // let our handler know.
openCallback(client); openCallback(client);
} }
return err; return err;
} }
PsychicEventSource * PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn) { PsychicEventSource* PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn)
{
_onOpen = fn; _onOpen = fn;
return this; return this;
} }
PsychicEventSource * PsychicEventSource::onClose(PsychicEventSourceClientCallback fn) { PsychicEventSource* PsychicEventSource::onClose(PsychicEventSourceClientCallback fn)
{
_onClose = fn; _onClose = fn;
return this; return this;
} }
void PsychicEventSource::addClient(PsychicClient *client) { void PsychicEventSource::addClient(PsychicClient* client)
{
client->_friend = new PsychicEventSourceClient(client); client->_friend = new PsychicEventSourceClient(client);
PsychicHandler::addClient(client); PsychicHandler::addClient(client);
} }
void PsychicEventSource::removeClient(PsychicClient *client) { void PsychicEventSource::removeClient(PsychicClient* client)
{
PsychicHandler::removeClient(client); PsychicHandler::removeClient(client);
delete (PsychicEventSourceClient*)client->_friend; delete (PsychicEventSourceClient*)client->_friend;
client->_friend = NULL; client->_friend = NULL;
} }
void PsychicEventSource::openCallback(PsychicClient *client) { void PsychicEventSource::openCallback(PsychicClient* client)
PsychicEventSourceClient *buddy = getClient(client); {
if (buddy == NULL) PsychicEventSourceClient* buddy = getClient(client);
{ if (buddy == NULL) {
return; return;
} }
@@ -103,10 +108,10 @@ void PsychicEventSource::openCallback(PsychicClient *client) {
_onOpen(buddy); _onOpen(buddy);
} }
void PsychicEventSource::closeCallback(PsychicClient *client) { void PsychicEventSource::closeCallback(PsychicClient* client)
PsychicEventSourceClient *buddy = getClient(client); {
if (buddy == NULL) PsychicEventSourceClient* buddy = getClient(client);
{ if (buddy == NULL) {
return; return;
} }
@@ -114,11 +119,13 @@ void PsychicEventSource::closeCallback(PsychicClient *client) {
_onClose(getClient(buddy)); _onClose(getClient(buddy));
} }
void PsychicEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) void PsychicEventSource::send(const char* message, const char* event, uint32_t id, uint32_t reconnect)
{ {
String ev = generateEventMessage(message, event, id, reconnect); String ev = generateEventMessage(message, event, id, reconnect);
for(PsychicClient *c : _clients) { for(PsychicClient *c : _clients) {
((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str()); if (c && c->_friend) {
((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str());
}
} }
} }
@@ -126,58 +133,135 @@ void PsychicEventSource::send(const char *message, const char *event, uint32_t i
// PsychicEventSourceClient // PsychicEventSourceClient
/*****************************************/ /*****************************************/
PsychicEventSourceClient::PsychicEventSourceClient(PsychicClient *client) : PsychicEventSourceClient::PsychicEventSourceClient(PsychicClient* client) : PsychicClient(client->server(), client->socket()),
PsychicClient(client->server(), client->socket()), _lastId(0)
_lastId(0)
{ {
} }
PsychicEventSourceClient::~PsychicEventSourceClient(){ PsychicEventSourceClient::~PsychicEventSourceClient()
{
} }
void PsychicEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){ void PsychicEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect)
{
String ev = generateEventMessage(message, event, id, reconnect); String ev = generateEventMessage(message, event, id, reconnect);
sendEvent(ev.c_str()); sendEvent(ev.c_str());
} }
void PsychicEventSourceClient::sendEvent(const char *event) { void PsychicEventSourceClient::sendEvent(const char* event)
int result; {
do { _sendEventAsync(this->server(), this->socket(), event, strlen(event));
result = httpd_socket_send(this->server(), this->socket(), event, strlen(event), 0); }
} while (result == HTTPD_SOCK_ERR_TIMEOUT);
//if (result < 0) esp_err_t PsychicEventSourceClient::_sendEventAsync(httpd_handle_t handle, int socket, const char* event, size_t len)
//error log here {
// create the transfer object
async_event_transfer_t* transfer = (async_event_transfer_t*)calloc(1, sizeof(async_event_transfer_t));
if (transfer == NULL) {
return ESP_ERR_NO_MEM;
}
// populate it
transfer->arg = this;
transfer->callback = _sendEventSentCallback;
transfer->handle = handle;
transfer->socket = socket;
transfer->len = len;
// allocate for event text
transfer->event = (char*)malloc(len);
if (transfer->event == NULL) {
free(transfer);
return ESP_ERR_NO_MEM;
}
// copy over the event data
memcpy(transfer->event, event, len);
// queue it.
esp_err_t err = httpd_queue_work(handle, _sendEventWorkCallback, transfer);
// cleanup
if (err) {
free(transfer->event);
free(transfer);
return err;
}
return ESP_OK;
}
void PsychicEventSourceClient::_sendEventWorkCallback(void* arg)
{
async_event_transfer_t* trans = (async_event_transfer_t*)arg;
// omg the error is overloaded with the number of bytes sent!
esp_err_t err = httpd_socket_send(trans->handle, trans->socket, trans->event, trans->len, 0);
if (err == trans->len)
err = ESP_OK;
if (trans->callback)
trans->callback(err, trans->socket, trans->arg);
// free our memory
free(trans->event);
free(trans);
}
void PsychicEventSourceClient::_sendEventSentCallback(esp_err_t err, int socket, void* arg)
{
// PsychicEventSourceClient* client = (PsychicEventSourceClient*)arg;
if (err == ESP_OK)
return;
else if (err == ESP_FAIL)
ESP_LOGE(PH_TAG, "EventSource: send - socket error (#%d)", socket);
else if (err == ESP_ERR_INVALID_STATE)
ESP_LOGE(PH_TAG, "EventSource: Handshake was already done beforehand (#%d)", socket);
else if (err == ESP_ERR_INVALID_ARG)
ESP_LOGE(PH_TAG, "EventSource: Argument is invalid (#%d)", socket);
else if (err == HTTPD_SOCK_ERR_TIMEOUT)
ESP_LOGE(PH_TAG, "EventSource: Socket timeout (#%d)", socket);
else if (err == HTTPD_SOCK_ERR_INVALID)
ESP_LOGE(PH_TAG, "EventSource: Invalid socket (#%d)", socket);
else if (err == HTTPD_SOCK_ERR_FAIL)
ESP_LOGE(PH_TAG, "EventSource: Socket fail (#%d)", socket);
else
ESP_LOGE(PH_TAG, "EventSource: %#06x %s (#%d)", (int)err, esp_err_to_name(err), socket);
} }
/*****************************************/ /*****************************************/
// PsychicEventSourceResponse // PsychicEventSourceResponse
/*****************************************/ /*****************************************/
PsychicEventSourceResponse::PsychicEventSourceResponse(PsychicRequest *request) PsychicEventSourceResponse::PsychicEventSourceResponse(PsychicResponse* response) : PsychicResponseDelegate(response)
: PsychicResponse(request)
{ {
} }
esp_err_t PsychicEventSourceResponse::send() { esp_err_t PsychicEventSourceResponse::send()
{
_response->addHeader("Content-Type", "text/event-stream");
_response->addHeader("Cache-Control", "no-cache");
_response->addHeader("Connection", "keep-alive");
//build our main header // build our main header
String out = String(); String out = String();
out.concat("HTTP/1.1 200 OK\r\n"); out.concat("HTTP/1.1 200 OK\r\n");
out.concat("Content-Type: text/event-stream\r\n");
out.concat("Cache-Control: no-cache\r\n");
out.concat("Connection: keep-alive\r\n");
//get our global headers out of the way first // get our global headers out of the way first
for (HTTPHeader header : DefaultHeaders::Instance().getHeaders()) for (auto& header : DefaultHeaders::Instance().getHeaders())
out.concat(String(header.field) + ": " + String(header.value) + "\r\n"); out.concat(header.field + ": " + header.value + "\r\n");
//separator // now do our individual headers
for (auto& header : _response->headers())
out.concat(header.field + ": " + header.value + "\r\n");
// separator
out.concat("\r\n"); out.concat("\r\n");
int result; int result;
do { do {
result = httpd_send(_request->request(), out.c_str(), out.length()); result = httpd_send(request(), out.c_str(), out.length());
} while (result == HTTPD_SOCK_ERR_TIMEOUT); } while (result == HTTPD_SOCK_ERR_TIMEOUT);
if (result < 0) if (result < 0)
@@ -193,28 +277,29 @@ esp_err_t PsychicEventSourceResponse::send() {
// Event Message Generator // Event Message Generator
/*****************************************/ /*****************************************/
String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect) { String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect)
{
String ev = ""; String ev = "";
if(reconnect){ if (reconnect) {
ev += "retry: "; ev += "retry: ";
ev += String(reconnect); ev += String(reconnect);
ev += "\r\n"; ev += "\r\n";
} }
if(id){ if (id) {
ev += "id: "; ev += "id: ";
ev += String(id); ev += String(id);
ev += "\r\n"; ev += "\r\n";
} }
if(event != NULL){ if (event != NULL) {
ev += "event: "; ev += "event: ";
ev += String(event); ev += String(event);
ev += "\r\n"; ev += "\r\n";
} }
if(message != NULL){ if (message != NULL) {
ev += "data: "; ev += "data: ";
ev += String(message); ev += String(message);
ev += "\r\n"; ev += "\r\n";

View File

@@ -20,9 +20,9 @@
#ifndef PsychicEventSource_H_ #ifndef PsychicEventSource_H_
#define PsychicEventSource_H_ #define PsychicEventSource_H_
#include "PsychicClient.h"
#include "PsychicCore.h" #include "PsychicCore.h"
#include "PsychicHandler.h" #include "PsychicHandler.h"
#include "PsychicClient.h"
#include "PsychicResponse.h" #include "PsychicResponse.h"
class PsychicEventSource; class PsychicEventSource;
@@ -30,24 +30,38 @@ class PsychicEventSourceResponse;
class PsychicEventSourceClient; class PsychicEventSourceClient;
class PsychicResponse; class PsychicResponse;
typedef std::function<void(PsychicEventSourceClient *client)> PsychicEventSourceClientCallback; typedef std::function<void(PsychicEventSourceClient* client)> PsychicEventSourceClientCallback;
class PsychicEventSourceClient : public PsychicClient { typedef struct {
friend PsychicEventSource; httpd_handle_t handle;
int socket;
char* event;
size_t len;
transfer_complete_cb callback;
void* arg;
} async_event_transfer_t;
class PsychicEventSourceClient : public PsychicClient
{
friend PsychicEventSource;
protected: protected:
uint32_t _lastId; uint32_t _lastId;
esp_err_t _sendEventAsync(httpd_handle_t handle, int socket, const char* event, size_t len);
static void _sendEventWorkCallback(void* arg);
static void _sendEventSentCallback(esp_err_t err, int socket, void* arg);
public: public:
PsychicEventSourceClient(PsychicClient *client); PsychicEventSourceClient(PsychicClient* client);
~PsychicEventSourceClient(); ~PsychicEventSourceClient();
uint32_t lastId() const { return _lastId; } uint32_t lastId() const { return _lastId; }
void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); void send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
void sendEvent(const char *event); void sendEvent(const char* event);
}; };
class PsychicEventSource : public PsychicHandler { class PsychicEventSource : public PsychicHandler
{
private: private:
PsychicEventSourceClientCallback _onOpen; PsychicEventSourceClientCallback _onOpen;
PsychicEventSourceClientCallback _onClose; PsychicEventSourceClientCallback _onClose;
@@ -56,27 +70,28 @@ class PsychicEventSource : public PsychicHandler {
PsychicEventSource(); PsychicEventSource();
~PsychicEventSource(); ~PsychicEventSource();
PsychicEventSourceClient * getClient(int socket) override; PsychicEventSourceClient* getClient(int socket) override;
PsychicEventSourceClient * getClient(PsychicClient *client) override; PsychicEventSourceClient* getClient(PsychicClient* client) override;
void addClient(PsychicClient *client) override; void addClient(PsychicClient* client) override;
void removeClient(PsychicClient *client) override; void removeClient(PsychicClient* client) override;
void openCallback(PsychicClient *client) override; void openCallback(PsychicClient* client) override;
void closeCallback(PsychicClient *client) override; void closeCallback(PsychicClient* client) override;
PsychicEventSource *onOpen(PsychicEventSourceClientCallback fn); PsychicEventSource* onOpen(PsychicEventSourceClientCallback fn);
PsychicEventSource *onClose(PsychicEventSourceClientCallback fn); PsychicEventSource* onClose(PsychicEventSourceClientCallback fn);
esp_err_t handleRequest(PsychicRequest *request) override final; esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override final;
void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); void send(const char* message, const char* event = NULL, uint32_t id = 0, uint32_t reconnect = 0);
}; };
class PsychicEventSourceResponse: public PsychicResponse { class PsychicEventSourceResponse : public PsychicResponseDelegate
{
public: public:
PsychicEventSourceResponse(PsychicRequest *request); PsychicEventSourceResponse(PsychicResponse* response);
virtual esp_err_t send() override; esp_err_t send();
}; };
String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect); String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect);
#endif /* PsychicEventSource_H_ */ #endif /* PsychicEventSource_H_ */

View File

@@ -1,97 +1,116 @@
#include "PsychicFileResponse.h" #include "PsychicFileResponse.h"
#include "PsychicResponse.h"
#include "PsychicRequest.h" #include "PsychicRequest.h"
#include "PsychicResponse.h"
PsychicFileResponse::PsychicFileResponse(PsychicResponse* response, FS& fs, const String& path, const String& contentType, bool download) : PsychicResponseDelegate(response)
PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType, bool download) {
: PsychicResponse(request) {
//_code = 200; //_code = 200;
String _path(path); String _path(path);
if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){ if (!download && !fs.exists(_path) && fs.exists(_path + ".gz")) {
_path = _path+".gz"; _path = _path + ".gz";
addHeader("Content-Encoding", "gzip"); addHeader("Content-Encoding", "gzip");
} }
_content = fs.open(_path, "r"); _content = fs.open(_path, "r");
_contentLength = _content.size(); setContentLength(_content.size());
if(contentType == "") if (contentType == "")
_setContentType(path); _setContentTypeFromPath(path);
else else
setContentType(contentType.c_str()); setContentType(contentType.c_str());
int filenameStart = path.lastIndexOf('/') + 1; int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart]; char buf[26 + path.length() - filenameStart];
char* filename = (char*)path.c_str() + filenameStart; char* filename = (char*)path.c_str() + filenameStart;
if(download) { if (download) {
// set filename and force download // set filename and force download
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", filename);
} else { } else {
// set filename and force rendering // set filename and force rendering
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); snprintf(buf, sizeof(buf), "inline; filename=\"%s\"", filename);
} }
addHeader("Content-Disposition", buf); addHeader("Content-Disposition", buf);
} }
PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType, bool download) PsychicFileResponse::PsychicFileResponse(PsychicResponse* response, File content, const String& path, const String& contentType, bool download) : PsychicResponseDelegate(response)
: PsychicResponse(request) { {
String _path(path); String _path(path);
if(!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")){ if (!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")) {
addHeader("Content-Encoding", "gzip"); addHeader("Content-Encoding", "gzip");
} }
_content = content; _content = content;
_contentLength = _content.size(); setContentLength(_content.size());
if(contentType == "") if (contentType == "")
_setContentType(path); _setContentTypeFromPath(path);
else else
setContentType(contentType.c_str()); setContentType(contentType.c_str());
int filenameStart = path.lastIndexOf('/') + 1; int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart]; char buf[26 + path.length() - filenameStart];
char* filename = (char*)path.c_str() + filenameStart; char* filename = (char*)path.c_str() + filenameStart;
if(download) { if (download) {
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", filename);
} else { } else {
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); snprintf(buf, sizeof(buf), "inline; filename=\"%s\"", filename);
} }
addHeader("Content-Disposition", buf); addHeader("Content-Disposition", buf);
} }
PsychicFileResponse::~PsychicFileResponse() PsychicFileResponse::~PsychicFileResponse()
{ {
if(_content) if (_content)
_content.close(); _content.close();
} }
void PsychicFileResponse::_setContentType(const String& path){ void PsychicFileResponse::_setContentTypeFromPath(const String& path)
const char *_contentType; {
const char* _contentType;
if (path.endsWith(".html")) _contentType = "text/html";
else if (path.endsWith(".htm")) _contentType = "text/html"; if (path.endsWith(".html"))
else if (path.endsWith(".css")) _contentType = "text/css"; _contentType = "text/html";
else if (path.endsWith(".json")) _contentType = "application/json"; else if (path.endsWith(".htm"))
else if (path.endsWith(".js")) _contentType = "application/javascript"; _contentType = "text/html";
else if (path.endsWith(".png")) _contentType = "image/png"; else if (path.endsWith(".css"))
else if (path.endsWith(".gif")) _contentType = "image/gif"; _contentType = "text/css";
else if (path.endsWith(".jpg")) _contentType = "image/jpeg"; else if (path.endsWith(".json"))
else if (path.endsWith(".ico")) _contentType = "image/x-icon"; _contentType = "application/json";
else if (path.endsWith(".svg")) _contentType = "image/svg+xml"; else if (path.endsWith(".js"))
else if (path.endsWith(".eot")) _contentType = "font/eot"; _contentType = "application/javascript";
else if (path.endsWith(".woff")) _contentType = "font/woff"; else if (path.endsWith(".png"))
else if (path.endsWith(".woff2")) _contentType = "font/woff2"; _contentType = "image/png";
else if (path.endsWith(".ttf")) _contentType = "font/ttf"; else if (path.endsWith(".gif"))
else if (path.endsWith(".xml")) _contentType = "text/xml"; _contentType = "image/gif";
else if (path.endsWith(".pdf")) _contentType = "application/pdf"; else if (path.endsWith(".jpg"))
else if (path.endsWith(".zip")) _contentType = "application/zip"; _contentType = "image/jpeg";
else if(path.endsWith(".gz")) _contentType = "application/x-gzip"; else if (path.endsWith(".ico"))
else _contentType = "text/plain"; _contentType = "image/x-icon";
else if (path.endsWith(".svg"))
_contentType = "image/svg+xml";
else if (path.endsWith(".eot"))
_contentType = "font/eot";
else if (path.endsWith(".woff"))
_contentType = "font/woff";
else if (path.endsWith(".woff2"))
_contentType = "font/woff2";
else if (path.endsWith(".ttf"))
_contentType = "font/ttf";
else if (path.endsWith(".xml"))
_contentType = "text/xml";
else if (path.endsWith(".pdf"))
_contentType = "application/pdf";
else if (path.endsWith(".zip"))
_contentType = "application/zip";
else if (path.endsWith(".gz"))
_contentType = "application/x-gzip";
else
_contentType = "text/plain";
setContentType(_contentType); setContentType(_contentType);
} }
@@ -99,59 +118,52 @@ esp_err_t PsychicFileResponse::send()
{ {
esp_err_t err = ESP_OK; esp_err_t err = ESP_OK;
//just send small files directly // just send small files directly
size_t size = getContentLength(); size_t size = getContentLength();
if (size < FILE_CHUNK_SIZE) if (size < FILE_CHUNK_SIZE) {
{ uint8_t* buffer = (uint8_t*)malloc(size);
uint8_t *buffer = (uint8_t *)malloc(size); if (buffer == NULL) {
if (buffer == NULL) ESP_LOGE(PH_TAG, "Unable to allocate %" PRIu32 " bytes to send chunk", size);
{ httpd_resp_send_err(request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
return ESP_FAIL; return ESP_FAIL;
} }
size_t readSize = _content.readBytes((char *)buffer, size); size_t readSize = _content.readBytes((char*)buffer, size);
setContent(buffer, readSize);
err = _response->send();
this->setContent(buffer, readSize);
err = PsychicResponse::send();
free(buffer); free(buffer);
} } else {
else
{
/* Retrieve the pointer to scratch buffer for temporary storage */ /* Retrieve the pointer to scratch buffer for temporary storage */
char *chunk = (char *)malloc(FILE_CHUNK_SIZE); char* chunk = (char*)malloc(FILE_CHUNK_SIZE);
if (chunk == NULL) if (chunk == NULL) {
{ ESP_LOGE(PH_TAG, "Unable to allocate %" PRIu32 " bytes to send chunk", FILE_CHUNK_SIZE);
/* Respond with 500 Internal Server Error */ httpd_resp_send_err(request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
return ESP_FAIL; return ESP_FAIL;
} }
this->sendHeaders(); sendHeaders();
size_t chunksize; size_t chunksize;
do { do {
/* Read file in chunks into the scratch buffer */ /* Read file in chunks into the scratch buffer */
chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE); chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE);
if (chunksize > 0) if (chunksize > 0) {
{ err = sendChunk((uint8_t*)chunk, chunksize);
err = this->sendChunk((uint8_t *)chunk, chunksize); if (err != ESP_OK)
if (err != ESP_OK) break;
break; }
}
/* Keep looping till the whole file is sent */ /* Keep looping till the whole file is sent */
} while (chunksize != 0); } while (chunksize != 0);
//keep track of our memory // keep track of our memory
free(chunk); free(chunk);
if (err == ESP_OK) if (err == ESP_OK) {
{
ESP_LOGD(PH_TAG, "File sending complete"); ESP_LOGD(PH_TAG, "File sending complete");
this->finishChunking(); finishChunking();
} }
} }

View File

@@ -6,16 +6,18 @@
class PsychicRequest; class PsychicRequest;
class PsychicFileResponse: public PsychicResponse class PsychicFileResponse : public PsychicResponseDelegate
{ {
using File = fs::File; using File = fs::File;
using FS = fs::FS; using FS = fs::FS;
private:
protected:
File _content; File _content;
void _setContentType(const String& path); void _setContentTypeFromPath(const String& path);
public: public:
PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType=String(), bool download=false); PsychicFileResponse(PsychicResponse* response, FS& fs, const String& path, const String& contentType = String(), bool download = false);
PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType=String(), bool download=false); PsychicFileResponse(PsychicResponse* response, File content, const String& path, const String& contentType = String(), bool download = false);
~PsychicFileResponse(); ~PsychicFileResponse();
esp_err_t send(); esp_err_t send();
}; };

View File

@@ -1,111 +1,148 @@
#include "PsychicHandler.h" #include "PsychicHandler.h"
PsychicHandler::PsychicHandler() : PsychicHandler::PsychicHandler()
_filter(NULL), {
_server(NULL), }
_username(""),
_password(""),
_method(DIGEST_AUTH),
_realm(""),
_authFailMsg(""),
_subprotocol("")
{}
PsychicHandler::~PsychicHandler() { PsychicHandler::~PsychicHandler()
{
delete _chain;
// actual PsychicClient deletion handled by PsychicServer // actual PsychicClient deletion handled by PsychicServer
// for (PsychicClient *client : _clients) // for (PsychicClient *client : _clients)
// delete(client); // delete(client);
_clients.clear(); _clients.clear();
} }
PsychicHandler* PsychicHandler::setFilter(PsychicRequestFilterFunction fn) { PsychicHandler* PsychicHandler::addFilter(PsychicRequestFilterFunction fn)
_filter = fn;
return this;
}
bool PsychicHandler::filter(PsychicRequest *request){
return _filter == NULL || _filter(request);
}
void PsychicHandler::setSubprotocol(const String& subprotocol) {
this->_subprotocol = subprotocol;
}
const char* PsychicHandler::getSubprotocol() const {
return _subprotocol.c_str();
}
PsychicHandler* PsychicHandler::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) {
_username = String(username);
_password = String(password);
_method = method;
_realm = String(realm);
_authFailMsg = String(authFailMsg);
return this;
};
bool PsychicHandler::needsAuthentication(PsychicRequest *request) {
return (_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str());
}
esp_err_t PsychicHandler::authenticate(PsychicRequest *request) {
return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str());
}
PsychicClient * PsychicHandler::checkForNewClient(PsychicClient *client)
{ {
PsychicClient *c = PsychicHandler::getClient(client); _filters.push_back(fn);
if (c == NULL) return this;
{ }
bool PsychicHandler::filter(PsychicRequest* request)
{
// run through our filter chain.
for (auto& filter : _filters) {
if (!filter(request)) {
ESP_LOGD(PH_TAG, "Request %s refused by filter from handler", request->uri().c_str());
return false;
}
}
return true;
}
void PsychicHandler::setSubprotocol(const String& subprotocol)
{
this->_subprotocol = subprotocol;
}
const char* PsychicHandler::getSubprotocol() const
{
return _subprotocol.c_str();
}
PsychicClient* PsychicHandler::checkForNewClient(PsychicClient* client)
{
PsychicClient* c = PsychicHandler::getClient(client);
if (c == NULL) {
c = client; c = client;
addClient(c); addClient(c);
c->isNew = true; c->isNew = true;
} } else
else
c->isNew = false; c->isNew = false;
return c; return c;
} }
void PsychicHandler::checkForClosedClient(PsychicClient *client) void PsychicHandler::checkForClosedClient(PsychicClient* client)
{ {
if (hasClient(client)) if (hasClient(client)) {
{
closeCallback(client); closeCallback(client);
removeClient(client); removeClient(client);
} }
} }
void PsychicHandler::addClient(PsychicClient *client) { void PsychicHandler::addClient(PsychicClient* client)
{
_clients.push_back(client); _clients.push_back(client);
} }
void PsychicHandler::removeClient(PsychicClient *client) { void PsychicHandler::removeClient(PsychicClient* client)
{
_clients.remove(client); _clients.remove(client);
} }
PsychicClient * PsychicHandler::getClient(int socket) PsychicClient* PsychicHandler::getClient(int socket)
{ {
//make sure the server has it too. // make sure the server has it too.
if (!_server->hasClient(socket)) if (!_server->hasClient(socket))
return NULL; return NULL;
//what about us? // what about us?
for (PsychicClient *client : _clients) for (PsychicClient* client : _clients)
if (client->socket() == socket) if (client->socket() == socket)
return client; return client;
//nothing found. // nothing found.
return NULL; return NULL;
} }
PsychicClient * PsychicHandler::getClient(PsychicClient *client) { PsychicClient* PsychicHandler::getClient(PsychicClient* client)
{
return PsychicHandler::getClient(client->socket()); return PsychicHandler::getClient(client->socket());
} }
bool PsychicHandler::hasClient(PsychicClient *socket) { bool PsychicHandler::hasClient(PsychicClient* socket)
{
return PsychicHandler::getClient(socket) != NULL; return PsychicHandler::getClient(socket) != NULL;
} }
const std::list<PsychicClient*>& PsychicHandler::getClientList() { const std::list<PsychicClient*>& PsychicHandler::getClientList()
{
return _clients; return _clients;
}
PsychicHandler* PsychicHandler::addMiddleware(PsychicMiddleware* middleware)
{
if (!_chain) {
_chain = new PsychicMiddlewareChain();
}
_chain->addMiddleware(middleware);
return this;
}
PsychicHandler* PsychicHandler::addMiddleware(PsychicMiddlewareCallback fn)
{
if (!_chain) {
_chain = new PsychicMiddlewareChain();
}
_chain->addMiddleware(fn);
return this;
}
void PsychicHandler::removeMiddleware(PsychicMiddleware* middleware)
{
if (_chain) {
_chain->removeMiddleware(middleware);
}
}
esp_err_t PsychicHandler::process(PsychicRequest* request)
{
if (!filter(request)) {
return HTTPD_404_NOT_FOUND;
}
if (!canHandle(request)) {
ESP_LOGD(PH_TAG, "Request %s refused by handler", request->uri().c_str());
return HTTPD_404_NOT_FOUND;
}
if (_chain) {
return _chain->runChain(request, [this, request]() {
return handleRequest(request, request->response());
});
} else {
return handleRequest(request, request->response());
}
} }

View File

@@ -6,23 +6,21 @@
class PsychicEndpoint; class PsychicEndpoint;
class PsychicHttpServer; class PsychicHttpServer;
class PsychicMiddleware;
class PsychicMiddlewareChain;
/* /*
* HANDLER :: Can be attached to any endpoint or as a generic request handler. * HANDLER :: Can be attached to any endpoint or as a generic request handler.
*/ */
class PsychicHandler { class PsychicHandler
friend PsychicEndpoint; {
friend PsychicEndpoint;
protected: protected:
PsychicRequestFilterFunction _filter; PsychicHttpServer* _server = nullptr;
PsychicHttpServer *_server; PsychicMiddlewareChain* _chain = nullptr;
std::list<PsychicRequestFilterFunction> _filters;
String _username;
String _password;
HTTPAuthMethod _method;
String _realm;
String _authFailMsg;
String _subprotocol; String _subprotocol;
@@ -32,35 +30,39 @@ class PsychicHandler {
PsychicHandler(); PsychicHandler();
virtual ~PsychicHandler(); virtual ~PsychicHandler();
PsychicHandler* setFilter(PsychicRequestFilterFunction fn);
bool filter(PsychicRequest *request);
PsychicHandler* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = "");
bool needsAuthentication(PsychicRequest *request);
esp_err_t authenticate(PsychicRequest *request);
virtual bool isWebSocket() { return false; }; virtual bool isWebSocket() { return false; };
void setSubprotocol(const String& subprotocol); void setSubprotocol(const String& subprotocol);
const char* getSubprotocol() const; const char* getSubprotocol() const;
PsychicClient * checkForNewClient(PsychicClient *client); PsychicClient* checkForNewClient(PsychicClient* client);
void checkForClosedClient(PsychicClient *client); void checkForClosedClient(PsychicClient* client);
virtual void addClient(PsychicClient *client); virtual void addClient(PsychicClient* client);
virtual void removeClient(PsychicClient *client); virtual void removeClient(PsychicClient* client);
virtual PsychicClient * getClient(int socket); virtual PsychicClient* getClient(int socket);
virtual PsychicClient * getClient(PsychicClient *client); virtual PsychicClient* getClient(PsychicClient* client);
virtual void openCallback(PsychicClient *client) {}; virtual void openCallback(PsychicClient* client) {};
virtual void closeCallback(PsychicClient *client) {}; virtual void closeCallback(PsychicClient* client) {};
bool hasClient(PsychicClient *client); bool hasClient(PsychicClient* client);
int count() { return _clients.size(); }; int count() { return _clients.size(); };
const std::list<PsychicClient*>& getClientList(); const std::list<PsychicClient*>& getClientList();
//derived classes must implement these functions // called to process this handler with its middleware chain and filers
virtual bool canHandle(PsychicRequest *request) { return true; }; esp_err_t process(PsychicRequest* request);
virtual esp_err_t handleRequest(PsychicRequest *request) = 0;
//bool filter(PsychicRequest* request);
PsychicHandler* addFilter(PsychicRequestFilterFunction fn);
bool filter(PsychicRequest* request);
PsychicHandler* addMiddleware(PsychicMiddleware* middleware);
PsychicHandler* addMiddleware(PsychicMiddlewareCallback fn);
void removeMiddleware(PsychicMiddleware *middleware);
// derived classes must implement these functions
virtual bool canHandle(PsychicRequest* request) { return true; };
virtual esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) { return HTTPD_404_NOT_FOUND; };
}; };
#endif #endif

View File

@@ -1,24 +1,33 @@
#ifndef PsychicHttp_h #ifndef PsychicHttp_h
#define PsychicHttp_h #define PsychicHttp_h
//#define ENABLE_ASYNC // This is something added in ESP-IDF 5.1.x where each request can be handled in its own thread // #define ENABLE_ASYNC // This is something added in ESP-IDF 5.1.x where each request can be handled in its own thread
#include <http_status.h> #include "PsychicEndpoint.h"
#include "PsychicEventSource.h"
#include "PsychicFileResponse.h"
#include "PsychicHandler.h"
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
#include "PsychicJson.h"
#include "PsychicMiddleware.h"
#include "PsychicMiddlewareChain.h"
#include "PsychicMiddlewares.h"
#include "PsychicRequest.h" #include "PsychicRequest.h"
#include "PsychicResponse.h" #include "PsychicResponse.h"
#include "PsychicEndpoint.h"
#include "PsychicHandler.h"
#include "PsychicStaticFileHandler.h" #include "PsychicStaticFileHandler.h"
#include "PsychicFileResponse.h"
#include "PsychicStreamResponse.h" #include "PsychicStreamResponse.h"
#include "PsychicUploadHandler.h" #include "PsychicUploadHandler.h"
#include "PsychicVersion.h"
#include "PsychicWebSocket.h" #include "PsychicWebSocket.h"
#include "PsychicEventSource.h" #include <http_status.h>
#include "PsychicJson.h"
#ifdef ENABLE_ASYNC #ifdef ENABLE_ASYNC
#include "async_worker.h" #include "async_worker.h"
#endif #endif
// debugging library
#ifdef PSY_USE_ARDUINO_TRACE
#include <ArduinoTrace.h>
#endif
#endif /* PsychicHttp_h */ #endif /* PsychicHttp_h */

View File

@@ -1,155 +1,305 @@
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
#include "PsychicEndpoint.h" #include "PsychicEndpoint.h"
#include "PsychicHandler.h" #include "PsychicHandler.h"
#include "PsychicWebHandler.h"
#include "PsychicStaticFileHandler.h"
#include "PsychicWebSocket.h"
#include "PsychicJson.h" #include "PsychicJson.h"
#include "PsychicStaticFileHandler.h"
#include "PsychicWebHandler.h"
#include "PsychicWebSocket.h"
#ifndef CONFIG_IDF_TARGET_ESP32H2
#include "WiFi.h" #include "WiFi.h"
#endif
PsychicHttpServer::PsychicHttpServer() : PsychicHttpServer::PsychicHttpServer(uint16_t port)
_onOpen(NULL),
_onClose(NULL)
{ {
maxRequestBodySize = MAX_REQUEST_BODY_SIZE; maxRequestBodySize = MAX_REQUEST_BODY_SIZE;
maxUploadSize = MAX_UPLOAD_SIZE; maxUploadSize = MAX_UPLOAD_SIZE;
defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, ""); defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, "");
onNotFound(PsychicHttpServer::defaultNotFoundHandler); onNotFound(PsychicHttpServer::defaultNotFoundHandler);
//for a regular server // for a regular server
config = HTTPD_DEFAULT_CONFIG(); config = HTTPD_DEFAULT_CONFIG();
config.open_fn = PsychicHttpServer::openCallback; config.open_fn = PsychicHttpServer::openCallback;
config.close_fn = PsychicHttpServer::closeCallback; config.close_fn = PsychicHttpServer::closeCallback;
config.uri_match_fn = httpd_uri_match_wildcard;
config.global_user_ctx = this; config.global_user_ctx = this;
config.global_user_ctx_free_fn = destroy; config.global_user_ctx_free_fn = PsychicHttpServer::destroy;
config.max_uri_handlers = 20; config.uri_match_fn = MATCH_WILDCARD; // new internal endpoint matching - do not change this!!!
config.stack_size = 4608; // default stack is just a little bit too small.
#ifdef ENABLE_ASYNC // our internal matching function for endpoints
// It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS _uri_match_fn = MATCH_WILDCARD; // use this change the endpoint matching function.
// Why? This leaves at least one socket still available to handle
// quick synchronous requests. Otherwise, all the sockets will #ifdef ENABLE_ASYNC
// get taken by the long async handlers, and your server will no // It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS
// longer be responsive. // Why? This leaves at least one socket still available to handle
config.max_open_sockets = ASYNC_WORKER_COUNT + 1; // quick synchronous requests. Otherwise, all the sockets will
config.lru_purge_enable = true; // get taken by the long async handlers, and your server will no
#endif // longer be responsive.
config.max_open_sockets = ASYNC_WORKER_COUNT + 1;
config.lru_purge_enable = true;
#endif
setPort(port);
} }
PsychicHttpServer::~PsychicHttpServer() PsychicHttpServer::~PsychicHttpServer()
{ {
for (auto *client : _clients) _esp_idf_endpoints.clear();
delete(client);
for (auto* client : _clients)
delete (client);
_clients.clear(); _clients.clear();
for (auto *endpoint : _endpoints) for (auto* endpoint : _endpoints)
delete(endpoint); delete (endpoint);
_endpoints.clear(); _endpoints.clear();
for (auto *handler : _handlers) for (auto* handler : _handlers)
delete(handler); delete (handler);
_handlers.clear(); _handlers.clear();
for (auto* rewrite : _rewrites)
delete (rewrite);
_rewrites.clear();
delete defaultEndpoint; delete defaultEndpoint;
delete _chain;
} }
void PsychicHttpServer::destroy(void *ctx) void PsychicHttpServer::destroy(void* ctx)
{ {
// do not release any resource for PsychicHttpServer in order to be able to restart it after stopping // do not release any resource for PsychicHttpServer in order to be able to restart it after stopping
} }
esp_err_t PsychicHttpServer::listen(uint16_t port) void PsychicHttpServer::setPort(uint16_t port)
{ {
this->_use_ssl = false;
this->config.server_port = port; this->config.server_port = port;
return this->_start();
} }
esp_err_t PsychicHttpServer::_start() uint16_t PsychicHttpServer::getPort()
{ {
return this->config.server_port;
}
esp_err_t PsychicHttpServer::start()
{
if (_running)
return ESP_OK;
esp_err_t ret; esp_err_t ret;
#ifdef ENABLE_ASYNC #ifdef ENABLE_ASYNC
// start workers // start workers
start_async_req_workers(); start_async_req_workers();
#endif #endif
//fire it up. // one URI handler for each http_method
config.max_uri_handlers = supported_methods.size() + _esp_idf_endpoints.size();
// fire it up.
ret = _startServer(); ret = _startServer();
if (ret != ESP_OK) if (ret != ESP_OK) {
{
ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret)); ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret));
return ret; return ret;
} }
// some handlers (aka websockets) need actual endpoints in esp-idf http_server
for (auto& endpoint : _esp_idf_endpoints) {
ESP_LOGD(PH_TAG, "Adding endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method));
// Register endpoint with ESP-IDF server
esp_err_t ret = httpd_register_uri_handler(this->server, &endpoint);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret));
}
// Register a handler for each http_method method - it will match all requests with that URI/method
for (auto& method : supported_methods) {
ESP_LOGD(PH_TAG, "Adding %s meta endpoint", http_method_str((http_method)method));
httpd_uri_t my_uri;
my_uri.uri = "*";
my_uri.method = method;
my_uri.handler = PsychicHttpServer::requestHandler;
my_uri.is_websocket = false;
my_uri.supported_subprotocol = "";
// Register endpoint with ESP-IDF server
esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret));
}
// Register handler // Register handler
ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler); ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler);
if (ret != ESP_OK) if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret)); ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret));
ESP_LOGI(PH_TAG, "Server started on port %" PRIu16, getPort());
_running = true;
return ret; return ret;
} }
esp_err_t PsychicHttpServer::_startServer() { esp_err_t PsychicHttpServer::_startServer()
{
return httpd_start(&this->server, &this->config); return httpd_start(&this->server, &this->config);
} }
void PsychicHttpServer::stop() esp_err_t PsychicHttpServer::stop()
{ {
httpd_stop(this->server); if (!_running)
return ESP_OK;
// some handlers (aka websockets) need actual endpoints in esp-idf http_server
for (auto& endpoint : _esp_idf_endpoints) {
ESP_LOGD(PH_TAG, "Removing endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method));
// Unregister endpoint with ESP-IDF server
esp_err_t ret = httpd_unregister_uri_handler(this->server, endpoint.uri, endpoint.method);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret));
}
// Unregister a handler for each http_method method - it will match all requests with that URI/method
for (auto& method : supported_methods) {
ESP_LOGD(PH_TAG, "Removing %s meta endpoint", http_method_str((http_method)method));
// Unregister endpoint with ESP-IDF server
esp_err_t ret = httpd_unregister_uri_handler(this->server, "*", method);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret));
}
esp_err_t ret = _stopServer();
if (ret != ESP_OK) {
ESP_LOGE(PH_TAG, "Server stop failed (%s)", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(PH_TAG, "Server stopped");
_running = false;
return ret;
} }
PsychicHandler& PsychicHttpServer::addHandler(PsychicHandler* handler){ esp_err_t PsychicHttpServer::_stopServer()
{
return httpd_stop(this->server);
}
void PsychicHttpServer::reset()
{
if (_running)
stop();
for (auto* client : _clients)
delete (client);
_clients.clear();
for (auto* endpoint : _endpoints)
delete (endpoint);
_endpoints.clear();
for (auto* handler : _handlers)
delete (handler);
_handlers.clear();
for (auto* rewrite : _rewrites)
delete (rewrite);
_rewrites.clear();
_esp_idf_endpoints.clear();
onNotFound(PsychicHttpServer::defaultNotFoundHandler);
_onOpen = nullptr;
_onClose = nullptr;
}
httpd_uri_match_func_t PsychicHttpServer::getURIMatchFunction()
{
return _uri_match_fn;
}
void PsychicHttpServer::setURIMatchFunction(httpd_uri_match_func_t match_fn)
{
_uri_match_fn = match_fn;
}
PsychicHandler* PsychicHttpServer::addHandler(PsychicHandler* handler)
{
_handlers.push_back(handler); _handlers.push_back(handler);
return *handler; return handler;
} }
void PsychicHttpServer::removeHandler(PsychicHandler *handler){ void PsychicHttpServer::removeHandler(PsychicHandler* handler)
{
_handlers.remove(handler); _handlers.remove(handler);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri) { PsychicRewrite* PsychicHttpServer::addRewrite(PsychicRewrite* rewrite)
{
_rewrites.push_back(rewrite);
return rewrite;
}
void PsychicHttpServer::removeRewrite(PsychicRewrite* rewrite)
{
_rewrites.remove(rewrite);
}
PsychicRewrite* PsychicHttpServer::rewrite(const char* from, const char* to)
{
return addRewrite(new PsychicRewrite(from, to));
}
PsychicEndpoint* PsychicHttpServer::on(const char* uri)
{
return on(uri, HTTP_GET); return on(uri, HTTP_GET);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method) PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method)
{ {
PsychicWebHandler *handler = new PsychicWebHandler(); PsychicWebHandler* handler = new PsychicWebHandler();
return on(uri, method, handler); return on(uri, method, handler);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler *handler) PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler* handler)
{ {
return on(uri, HTTP_GET, handler); return on(uri, HTTP_GET, handler);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHandler *handler) PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHandler* handler)
{ {
//make our endpoint // make our endpoint
PsychicEndpoint *endpoint = new PsychicEndpoint(this, method, uri); PsychicEndpoint* endpoint = new PsychicEndpoint(this, method, uri);
//set our handler // set our handler
endpoint->setHandler(handler); endpoint->setHandler(handler);
// URI handler structure // websockets need a real endpoint in esp-idf
httpd_uri_t my_uri { if (handler->isWebSocket()) {
.uri = uri, // URI handler structure
.method = method, httpd_uri_t my_uri;
.handler = PsychicEndpoint::requestCallback, my_uri.uri = uri;
.user_ctx = endpoint, my_uri.method = HTTP_GET;
.is_websocket = handler->isWebSocket(), my_uri.handler = PsychicEndpoint::requestCallback;
.supported_subprotocol = handler->getSubprotocol() my_uri.user_ctx = endpoint;
}; my_uri.is_websocket = handler->isWebSocket();
my_uri.supported_subprotocol = handler->getSubprotocol();
// Register endpoint with ESP-IDF server
esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret));
//save it for later // save it to our 'real' handlers for later.
_esp_idf_endpoints.push_back(my_uri);
}
// if this is a method we haven't added yet, do it.
if (method != HTTP_ANY) {
if (!(std::find(supported_methods.begin(), supported_methods.end(), (http_method)method) != supported_methods.end())) {
ESP_LOGD(PH_TAG, "Adding %s to server.supported_methods", http_method_str((http_method)method));
supported_methods.push_back((http_method)method);
}
}
// add it to our meta endpoints
_endpoints.push_back(endpoint); _endpoints.push_back(endpoint);
return endpoint; return endpoint;
@@ -160,10 +310,10 @@ PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallba
return on(uri, HTTP_GET, fn); return on(uri, HTTP_GET, fn);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHttpRequestCallback fn) PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHttpRequestCallback fn)
{ {
//these basic requests need a basic web handler // these basic requests need a basic web handler
PsychicWebHandler *handler = new PsychicWebHandler(); PsychicWebHandler* handler = new PsychicWebHandler();
handler->onRequest(fn); handler->onRequest(fn);
return on(uri, method, handler); return on(uri, method, handler);
@@ -174,59 +324,187 @@ PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallba
return on(uri, HTTP_GET, fn); return on(uri, HTTP_GET, fn);
} }
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicJsonRequestCallback fn) PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicJsonRequestCallback fn)
{ {
//these basic requests need a basic web handler // these basic requests need a basic web handler
PsychicJsonHandler *handler = new PsychicJsonHandler(); PsychicJsonHandler* handler = new PsychicJsonHandler();
handler->onRequest(fn); handler->onRequest(fn);
return on(uri, method, handler); return on(uri, method, handler);
} }
bool PsychicHttpServer::removeEndpoint(const char* uri, int method)
{
// some handlers (aka websockets) need actual endpoints in esp-idf http_server
// don't return from here, because its added to the _endpoints list too.
for (auto& endpoint : _esp_idf_endpoints) {
if (!strcmp(endpoint.uri, uri) && method == endpoint.method) {
ESP_LOGD(PH_TAG, "Unregistering endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method));
// Register endpoint with ESP-IDF server
esp_err_t ret = httpd_register_uri_handler(this->server, &endpoint);
if (ret != ESP_OK)
ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret));
}
}
// loop through our endpoints and see if anyone matches
for (auto* endpoint : _endpoints) {
if (endpoint->uri().equals(uri) && method == endpoint->_method)
return removeEndpoint(endpoint);
}
return false;
}
bool PsychicHttpServer::removeEndpoint(PsychicEndpoint* endpoint)
{
_endpoints.remove(endpoint);
return true;
}
PsychicHttpServer* PsychicHttpServer::addFilter(PsychicRequestFilterFunction fn)
{
_filters.push_back(fn);
return this;
}
bool PsychicHttpServer::_filter(PsychicRequest* request)
{
// run through our filter chain.
for (auto& filter : _filters) {
if (!filter(request))
return false;
}
return true;
}
PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddleware* middleware)
{
if (!_chain) {
_chain = new PsychicMiddlewareChain();
}
_chain->addMiddleware(middleware);
return this;
}
PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddlewareCallback fn)
{
if (!_chain) {
_chain = new PsychicMiddlewareChain();
}
_chain->addMiddleware(fn);
return this;
}
void PsychicHttpServer::removeMiddleware(PsychicMiddleware* middleware)
{
if (_chain) {
_chain->removeMiddleware(middleware);
}
}
void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn) void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn)
{ {
PsychicWebHandler *handler = new PsychicWebHandler(); PsychicWebHandler* handler = new PsychicWebHandler();
handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn); handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn);
this->defaultEndpoint->setHandler(handler); this->defaultEndpoint->setHandler(handler);
} }
esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t *req, httpd_err_code_t err) bool PsychicHttpServer::_rewriteRequest(PsychicRequest* request)
{ {
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); for (auto* r : _rewrites) {
PsychicRequest request(server, req); if (r->match(request)) {
request->_setUri(r->toUrl().c_str());
//loop through our global handlers and see if anyone wants it return true;
for(auto *handler: server->_handlers)
{
//are we capable of handling this?
if (handler->filter(&request) && handler->canHandle(&request))
{
//check our credentials
if (handler->needsAuthentication(&request))
return handler->authenticate(&request);
else
return handler->handleRequest(&request);
} }
} }
//nothing found, give it to our defaultEndpoint return false;
PsychicHandler *handler = server->defaultEndpoint->handler();
if (handler->filter(&request) && handler->canHandle(&request))
return handler->handleRequest(&request);
//not sure how we got this far.
return ESP_ERR_HTTPD_INVALID_REQ;
} }
esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest *request) esp_err_t PsychicHttpServer::requestHandler(httpd_req_t* req)
{ {
request->reply(404, "text/html", "That URI does not exist."); PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle);
PsychicRequest request(server, req);
return ESP_OK; // process any URL rewrites
server->_rewriteRequest(&request);
// run it through our global server filter list
if (!server->_filter(&request)) {
ESP_LOGD(PH_TAG, "Request %s refused by global filter", request.uri().c_str());
return request.response()->send(400);
}
// then runs the request through the filter chain
esp_err_t ret;
if (server->_chain) {
ret = server->_chain->runChain(&request, [server, &request]() {
return server->_process(&request);
});
} else {
ret = server->_process(&request);
}
ESP_LOGD(PH_TAG, "Request %s processed by global middleware: %s", request.uri().c_str(), esp_err_to_name(ret));
if (ret == HTTPD_404_NOT_FOUND) {
return PsychicHttpServer::notFoundHandler(req, HTTPD_404_NOT_FOUND);
}
return ret;
} }
void PsychicHttpServer::onOpen(PsychicClientCallback handler) { esp_err_t PsychicHttpServer::_process(PsychicRequest* request)
{
// loop through our endpoints and see if anyone wants it.
for (auto* endpoint : _endpoints) {
if (endpoint->matches(request->uri().c_str())) {
if (endpoint->_method == request->method() || endpoint->_method == HTTP_ANY) {
request->setEndpoint(endpoint);
return endpoint->process(request);
}
}
}
// loop through our global handlers and see if anyone wants it
for (auto* handler : _handlers) {
esp_err_t ret = handler->process(request);
if (ret != HTTPD_404_NOT_FOUND)
return ret;
}
return HTTPD_404_NOT_FOUND;
}
esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t* req, httpd_err_code_t err)
{
PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle);
PsychicRequest request(server, req);
// pull up our default handler / endpoint
PsychicHandler* handler = server->defaultEndpoint->handler();
if (!handler)
return request.response()->send(404);
esp_err_t ret = handler->process(&request);
if (ret != HTTPD_404_NOT_FOUND)
return ret;
// not sure how we got this far.
return request.response()->send(404);
}
esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest* request, PsychicResponse* response)
{
return response->send(404, "text/html", "That URI does not exist.");
}
void PsychicHttpServer::onOpen(PsychicClientCallback handler)
{
this->_onOpen = handler; this->_onOpen = handler;
} }
@@ -234,25 +512,25 @@ esp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd)
{ {
ESP_LOGD(PH_TAG, "New client connected %d", sockfd); ESP_LOGD(PH_TAG, "New client connected %d", sockfd);
//get our global server reference // get our global server reference
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);
//lookup our client // lookup our client
PsychicClient *client = server->getClient(sockfd); PsychicClient* client = server->getClient(sockfd);
if (client == NULL) if (client == NULL) {
{
client = new PsychicClient(hd, sockfd); client = new PsychicClient(hd, sockfd);
server->addClient(client); server->addClient(client);
} }
//user callback // user callback
if (server->_onOpen != NULL) if (server->_onOpen != NULL)
server->_onOpen(client); server->_onOpen(client);
return ESP_OK; return ESP_OK;
} }
void PsychicHttpServer::onClose(PsychicClientCallback handler) { void PsychicHttpServer::onClose(PsychicClientCallback handler)
{
this->_onClose = handler; this->_onClose = handler;
} }
@@ -260,30 +538,27 @@ void PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd)
{ {
ESP_LOGD(PH_TAG, "Client disconnected %d", sockfd); ESP_LOGD(PH_TAG, "Client disconnected %d", sockfd);
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);
//lookup our client // lookup our client
PsychicClient *client = server->getClient(sockfd); PsychicClient* client = server->getClient(sockfd);
if (client != NULL) if (client != NULL) {
{ // give our handlers a chance to handle a disconnect first
//give our handlers a chance to handle a disconnect first for (PsychicEndpoint* endpoint : server->_endpoints) {
for (PsychicEndpoint * endpoint : server->_endpoints) PsychicHandler* handler = endpoint->handler();
{
PsychicHandler *handler = endpoint->handler();
handler->checkForClosedClient(client); handler->checkForClosedClient(client);
} }
//do we have a callback attached? // do we have a callback attached?
if (server->_onClose != NULL) if (server->_onClose != NULL)
server->_onClose(client); server->_onClose(client);
//remove it from our list // remove it from our list
server->removeClient(client); server->removeClient(client);
} } else
else
ESP_LOGE(PH_TAG, "No client record %d", sockfd); ESP_LOGE(PH_TAG, "No client record %d", sockfd);
//finally close it out. // finally close it out.
close(sockfd); close(sockfd);
} }
@@ -295,48 +570,56 @@ PsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS
return handler; return handler;
} }
void PsychicHttpServer::addClient(PsychicClient *client) { void PsychicHttpServer::addClient(PsychicClient* client)
{
_clients.push_back(client); _clients.push_back(client);
} }
void PsychicHttpServer::removeClient(PsychicClient *client) { void PsychicHttpServer::removeClient(PsychicClient* client)
{
_clients.remove(client); _clients.remove(client);
delete client; delete client;
} }
PsychicClient * PsychicHttpServer::getClient(int socket) { PsychicClient* PsychicHttpServer::getClient(int socket)
for (PsychicClient * client : _clients) {
for (PsychicClient* client : _clients)
if (client->socket() == socket) if (client->socket() == socket)
return client; return client;
return NULL; return NULL;
} }
PsychicClient * PsychicHttpServer::getClient(httpd_req_t *req) { PsychicClient* PsychicHttpServer::getClient(httpd_req_t* req)
{
return getClient(httpd_req_to_sockfd(req)); return getClient(httpd_req_to_sockfd(req));
} }
bool PsychicHttpServer::hasClient(int socket) { bool PsychicHttpServer::hasClient(int socket)
{
return getClient(socket) != NULL; return getClient(socket) != NULL;
} }
const std::list<PsychicClient*>& PsychicHttpServer::getClientList() { const std::list<PsychicClient*>& PsychicHttpServer::getClientList()
{
return _clients; return _clients;
} }
bool ON_STA_FILTER(PsychicRequest *request) { bool ON_STA_FILTER(PsychicRequest* request)
#ifndef CONFIG_IDF_TARGET_ESP32H2 {
return WiFi.localIP() == request->client()->localIP(); #if defined(CONFIG_IDF_TARGET_ESP32H2)
#else
return false; return false;
#else
return WiFi.localIP() == request->client()->localIP();
#endif #endif
} }
bool ON_AP_FILTER(PsychicRequest *request) { bool ON_AP_FILTER(PsychicRequest* request)
#ifndef CONFIG_IDF_TARGET_ESP32H2 {
return WiFi.softAPIP() == request->client()->localIP(); #if defined(CONFIG_IDF_TARGET_ESP32H2)
#else
return false; return false;
#else
return WiFi.softAPIP() == request->client()->localIP();
#endif #endif
} }
@@ -350,25 +633,46 @@ String urlDecode(const char* encoded)
size_t i, j = 0; size_t i, j = 0;
for (i = 0; i < length; ++i) { for (i = 0; i < length; ++i) {
if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) { if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) {
// Valid percent-encoded sequence // Valid percent-encoded sequence
int hex; int hex;
sscanf(encoded + i + 1, "%2x", &hex); sscanf(encoded + i + 1, "%2x", &hex);
decoded[j++] = (char)hex; decoded[j++] = (char)hex;
i += 2; // Skip the two hexadecimal characters i += 2; // Skip the two hexadecimal characters
} else if (encoded[i] == '+') { } else if (encoded[i] == '+') {
// Convert '+' to space // Convert '+' to space
decoded[j++] = ' '; decoded[j++] = ' ';
} else { } else {
// Copy other characters as they are // Copy other characters as they are
decoded[j++] = encoded[i]; decoded[j++] = encoded[i];
} }
} }
decoded[j] = '\0'; // Null-terminate the decoded string decoded[j] = '\0'; // Null-terminate the decoded string
String output(decoded); String output(decoded);
free(decoded); free(decoded);
return output; return output;
} }
bool psychic_uri_match_simple(const char* uri1, const char* uri2, size_t len2)
{
return strlen(uri1) == len2 && // First match lengths
(strncmp(uri1, uri2, len2) == 0); // Then match actual URIs
}
#ifdef PSY_ENABLE_REGEX
bool psychic_uri_match_regex(const char* uri1, const char* uri2, size_t len2)
{
std::regex pattern(uri1);
std::smatch matches;
std::string s(uri2);
// len2 is passed in to tell us to match up to a point.
if (s.length() > len2)
s = s.substr(0, len2);
return std::regex_search(s, matches, pattern);
}
#endif

View File

@@ -1,9 +1,20 @@
#ifndef PsychicHttpServer_h #ifndef PsychicHttpServer_h
#define PsychicHttpServer_h #define PsychicHttpServer_h
#include "PsychicCore.h"
#include "PsychicClient.h" #include "PsychicClient.h"
#include "PsychicCore.h"
#include "PsychicHandler.h" #include "PsychicHandler.h"
#include "PsychicMiddleware.h"
#include "PsychicMiddlewareChain.h"
#include "PsychicRewrite.h"
#ifdef PSY_ENABLE_REGEX
#include <regex>
#endif
#ifndef HTTP_ANY
#define HTTP_ANY INT_MAX
#endif
class PsychicEndpoint; class PsychicEndpoint;
class PsychicHandler; class PsychicHandler;
@@ -12,70 +23,123 @@ class PsychicStaticFileHandler;
class PsychicHttpServer class PsychicHttpServer
{ {
protected: protected:
bool _use_ssl = false; std::list<httpd_uri_t> _esp_idf_endpoints;
std::list<PsychicEndpoint*> _endpoints; std::list<PsychicEndpoint*> _endpoints;
std::list<PsychicHandler*> _handlers; std::list<PsychicHandler*> _handlers;
std::list<PsychicClient*> _clients; std::list<PsychicClient*> _clients;
std::list<PsychicRewrite*> _rewrites;
std::list<PsychicRequestFilterFunction> _filters;
PsychicClientCallback _onOpen; PsychicClientCallback _onOpen = nullptr;
PsychicClientCallback _onClose; PsychicClientCallback _onClose = nullptr;
PsychicMiddlewareChain* _chain = nullptr;
esp_err_t _start(); esp_err_t _start();
virtual esp_err_t _startServer(); virtual esp_err_t _startServer();
virtual esp_err_t _stopServer();
bool _running = false;
httpd_uri_match_func_t _uri_match_fn = nullptr;
bool _rewriteRequest(PsychicRequest* request);
esp_err_t _process(PsychicRequest* request);
bool _filter(PsychicRequest* request);
public: public:
PsychicHttpServer(); PsychicHttpServer(uint16_t port = 80);
virtual ~PsychicHttpServer(); virtual ~PsychicHttpServer();
//esp-idf specific stuff // what methods to support
std::list<http_method> supported_methods = {
HTTP_GET,
HTTP_POST,
HTTP_DELETE,
HTTP_HEAD,
HTTP_PUT,
HTTP_OPTIONS
};
// esp-idf specific stuff
httpd_handle_t server; httpd_handle_t server;
httpd_config_t config; httpd_config_t config;
//some limits on what we will accept // some limits on what we will accept
unsigned long maxUploadSize; unsigned long maxUploadSize;
unsigned long maxRequestBodySize; unsigned long maxRequestBodySize;
PsychicEndpoint *defaultEndpoint; PsychicEndpoint* defaultEndpoint;
static void destroy(void *ctx); static void destroy(void* ctx);
esp_err_t listen(uint16_t port); virtual void setPort(uint16_t port);
virtual uint16_t getPort();
virtual void stop(); bool isRunning() { return _running; }
esp_err_t begin() { return start(); }
esp_err_t end() { return stop(); }
esp_err_t start();
esp_err_t stop();
void reset();
PsychicHandler& addHandler(PsychicHandler* handler); httpd_uri_match_func_t getURIMatchFunction();
void setURIMatchFunction(httpd_uri_match_func_t match_fn);
PsychicRewrite* addRewrite(PsychicRewrite* rewrite);
void removeRewrite(PsychicRewrite* rewrite);
PsychicRewrite* rewrite(const char* from, const char* to);
PsychicHandler* addHandler(PsychicHandler* handler);
void removeHandler(PsychicHandler* handler); void removeHandler(PsychicHandler* handler);
void addClient(PsychicClient *client); void addClient(PsychicClient* client);
void removeClient(PsychicClient *client); void removeClient(PsychicClient* client);
PsychicClient* getClient(int socket); PsychicClient* getClient(int socket);
PsychicClient* getClient(httpd_req_t *req); PsychicClient* getClient(httpd_req_t* req);
bool hasClient(int socket); bool hasClient(int socket);
int count() { return _clients.size(); }; int count() { return _clients.size(); };
const std::list<PsychicClient*>& getClientList(); const std::list<PsychicClient*>& getClientList();
PsychicEndpoint* on(const char* uri); PsychicEndpoint* on(const char* uri);
PsychicEndpoint* on(const char* uri, http_method method); PsychicEndpoint* on(const char* uri, int method);
PsychicEndpoint* on(const char* uri, PsychicHandler *handler); PsychicEndpoint* on(const char* uri, PsychicHandler* handler);
PsychicEndpoint* on(const char* uri, http_method method, PsychicHandler *handler); PsychicEndpoint* on(const char* uri, int method, PsychicHandler* handler);
PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest); PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest);
PsychicEndpoint* on(const char* uri, http_method method, PsychicHttpRequestCallback onRequest); PsychicEndpoint* on(const char* uri, int method, PsychicHttpRequestCallback onRequest);
PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest); PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest);
PsychicEndpoint* on(const char* uri, http_method method, PsychicJsonRequestCallback onRequest); PsychicEndpoint* on(const char* uri, int method, PsychicJsonRequestCallback onRequest);
static esp_err_t notFoundHandler(httpd_req_t *req, httpd_err_code_t err); bool removeEndpoint(const char* uri, int method);
static esp_err_t defaultNotFoundHandler(PsychicRequest *request); bool removeEndpoint(PsychicEndpoint* endpoint);
void onNotFound(PsychicHttpRequestCallback fn);
void onOpen(PsychicClientCallback handler); PsychicHttpServer* addFilter(PsychicRequestFilterFunction fn);
void onClose(PsychicClientCallback handler);
PsychicHttpServer* addMiddleware(PsychicMiddleware* middleware);
PsychicHttpServer* addMiddleware(PsychicMiddlewareCallback fn);
void removeMiddleware(PsychicMiddleware *middleware);
static esp_err_t requestHandler(httpd_req_t* req);
static esp_err_t notFoundHandler(httpd_req_t* req, httpd_err_code_t err);
static esp_err_t defaultNotFoundHandler(PsychicRequest* request, PsychicResponse* response);
static esp_err_t openCallback(httpd_handle_t hd, int sockfd); static esp_err_t openCallback(httpd_handle_t hd, int sockfd);
static void closeCallback(httpd_handle_t hd, int sockfd); static void closeCallback(httpd_handle_t hd, int sockfd);
void onNotFound(PsychicHttpRequestCallback fn);
void onOpen(PsychicClientCallback handler);
void onClose(PsychicClientCallback handler);
PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL);
}; };
bool ON_STA_FILTER(PsychicRequest *request); bool ON_STA_FILTER(PsychicRequest* request);
bool ON_AP_FILTER(PsychicRequest *request); bool ON_AP_FILTER(PsychicRequest* request);
// URI matching functions
bool psychic_uri_match_simple(const char* uri1, const char* uri2, size_t len2);
#define MATCH_SIMPLE psychic_uri_match_simple
#define MATCH_WILDCARD httpd_uri_match_wildcard
#ifdef PSY_ENABLE_REGEX
bool psychic_uri_match_regex(const char* uri1, const char* uri2, size_t len2);
#define MATCH_REGEX psychic_uri_match_regex
#endif
#endif // PsychicHttpServer_h #endif // PsychicHttpServer_h

View File

@@ -2,9 +2,9 @@
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
PsychicHttpsServer::PsychicHttpsServer() : PsychicHttpServer() PsychicHttpsServer::PsychicHttpsServer(uint16_t port) : PsychicHttpServer(port)
{ {
//for a SSL server // for a SSL server
ssl_config = HTTPD_SSL_CONFIG_DEFAULT(); ssl_config = HTTPD_SSL_CONFIG_DEFAULT();
ssl_config.httpd.open_fn = PsychicHttpServer::openCallback; ssl_config.httpd.open_fn = PsychicHttpServer::openCallback;
ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback; ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback;
@@ -18,44 +18,53 @@ PsychicHttpsServer::PsychicHttpsServer() : PsychicHttpServer()
// if we set it higher than 2 and use all the connections, we get lots of memory errors. // if we set it higher than 2 and use all the connections, we get lots of memory errors.
// not to mention there is no heap left over for the program itself. // not to mention there is no heap left over for the program itself.
ssl_config.httpd.max_open_sockets = 2; ssl_config.httpd.max_open_sockets = 2;
setPort(port);
} }
PsychicHttpsServer::~PsychicHttpsServer() {} PsychicHttpsServer::~PsychicHttpsServer() {}
esp_err_t PsychicHttpsServer::listen(uint16_t port, const char *cert, const char *private_key) void PsychicHttpsServer::setPort(uint16_t port)
{ {
this->_use_ssl = true;
this->ssl_config.port_secure = port; this->ssl_config.port_secure = port;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2) uint16_t PsychicHttpsServer::getPort()
this->ssl_config.servercert = (uint8_t *)cert; {
this->ssl_config.servercert_len = strlen(cert)+1; return this->ssl_config.port_secure;
#else }
this->ssl_config.cacert_pem = (uint8_t *)cert;
this->ssl_config.cacert_len = strlen(cert)+1;
#endif
this->ssl_config.prvtkey_pem = (uint8_t *)private_key; void PsychicHttpsServer::setCertificate(const uint8_t* cert, size_t cert_size, const uint8_t* private_key, size_t private_key_size)
this->ssl_config.prvtkey_len = strlen(private_key)+1; {
if (cert) {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2)
this->ssl_config.servercert = cert;
this->ssl_config.servercert_len = cert_size;
#else
this->ssl_config.cacert_pem = cert;
this->ssl_config.cacert_len = cert_size;
#endif
}
return this->_start(); if (private_key) {
this->ssl_config.prvtkey_pem = private_key;
this->ssl_config.prvtkey_len = private_key_size;
}
} }
esp_err_t PsychicHttpsServer::_startServer() esp_err_t PsychicHttpsServer::_startServer()
{ {
if (this->_use_ssl) return httpd_ssl_start(&this->server, &this->ssl_config);
return httpd_ssl_start(&this->server, &this->ssl_config);
else
return httpd_start(&this->server, &this->config);
} }
void PsychicHttpsServer::stop() esp_err_t PsychicHttpsServer::_stopServer()
{ {
if (this->_use_ssl) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2)
httpd_ssl_stop(this->server); return httpd_ssl_stop(this->server);
else #else
httpd_stop(this->server); httpd_ssl_stop(this->server);
return ESP_OK;
#endif
} }
#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE #endif // CONFIG_ESP_HTTPS_SERVER_ENABLE

View File

@@ -1,38 +1,44 @@
#ifndef PsychicHttpsServer_h #ifndef PsychicHttpsServer_h
#define PsychicHttpsServer_h #define PsychicHttpsServer_h
#include <sdkconfig.h> #include <sdkconfig.h>
#ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE
#include "PsychicCore.h" #include "PsychicCore.h"
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
#include <esp_https_server.h> #include <esp_https_server.h>
#if !CONFIG_HTTPD_WS_SUPPORT #if !CONFIG_HTTPD_WS_SUPPORT
#error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration #error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration
#endif #endif
#define PSY_ENABLE_SSL //you can use this define in your code to enable/disable these features #ifndef PSY_ENABLE_SSL
#define PSY_ENABLE_SSL // you can use this define in your code to enable/disable these features
#endif
class PsychicHttpsServer : public PsychicHttpServer class PsychicHttpsServer : public PsychicHttpServer
{ {
protected: protected:
bool _use_ssl = false; virtual esp_err_t _startServer() override final;
virtual esp_err_t _stopServer() override final;
public: public:
PsychicHttpsServer(); PsychicHttpsServer(uint16_t port = 443);
~PsychicHttpsServer(); ~PsychicHttpsServer();
httpd_ssl_config_t ssl_config; httpd_ssl_config_t ssl_config;
using PsychicHttpServer::listen; //keep the regular version // using PsychicHttpServer::listen; // keep the regular version
esp_err_t listen(uint16_t port, const char *cert, const char *private_key); virtual void setPort(uint16_t port) override final;
virtual uint16_t getPort() override final;
virtual esp_err_t _startServer() override final; // Pointer to certificate data in PEM format
virtual void stop() override final; void setCertificate(const char* cert, const char* private_key) { setCertificate((const uint8_t*)cert, strlen(cert) + 1, (const uint8_t*)private_key, private_key ? strlen(private_key) + 1 : 0); }
// Pointer to certificate data in PEM or DER format. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in certSize and keySize.
void setCertificate(const uint8_t* cert, size_t cert_size, const uint8_t* private_key, size_t private_key_size);
}; };
#endif // PsychicHttpsServer_h
#else #else
#warning ESP-IDF https server support not enabled. #warning ESP-IDF https server support not enabled.
#endif // CONFIG_ESP_HTTPS_SERVER_ENABLE #endif // CONFIG_ESP_HTTPS_SERVER_ENABLE
#endif // PsychicHttpsServer_h

View File

@@ -1,28 +1,30 @@
#include "PsychicJson.h" #include "PsychicJson.h"
#ifdef ARDUINOJSON_6_COMPATIBILITY #ifdef ARDUINOJSON_6_COMPATIBILITY
PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray, size_t maxJsonBufferSize) : PsychicJsonResponse::PsychicJsonResponse(PsychicResponse* response, bool isArray, size_t maxJsonBufferSize) : __response(response),
PsychicResponse(request), _jsonBuffer(maxJsonBufferSize)
_jsonBuffer(maxJsonBufferSize) {
{ response->setContentType(JSON_MIMETYPE);
setContentType(JSON_MIMETYPE); if (isArray)
if (isArray) _root = _jsonBuffer.createNestedArray();
_root = _jsonBuffer.createNestedArray(); else
else _root = _jsonBuffer.createNestedObject();
_root = _jsonBuffer.createNestedObject(); }
}
#else #else
PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray) : PsychicResponse(request) PsychicJsonResponse::PsychicJsonResponse(PsychicResponse* response, bool isArray) : PsychicResponseDelegate(response)
{ {
setContentType(JSON_MIMETYPE); setContentType(JSON_MIMETYPE);
if (isArray) if (isArray)
_root = _jsonBuffer.add<JsonArray>(); _root = _jsonBuffer.add<JsonArray>();
else else
_root = _jsonBuffer.add<JsonObject>(); _root = _jsonBuffer.add<JsonObject>();
} }
#endif #endif
JsonVariant &PsychicJsonResponse::getRoot() { return _root; } JsonVariant& PsychicJsonResponse::getRoot()
{
return _root;
}
size_t PsychicJsonResponse::getLength() size_t PsychicJsonResponse::getLength()
{ {
@@ -34,100 +36,93 @@ esp_err_t PsychicJsonResponse::send()
esp_err_t err = ESP_OK; esp_err_t err = ESP_OK;
size_t length = getLength(); size_t length = getLength();
size_t buffer_size; size_t buffer_size;
char *buffer; char* buffer;
//how big of a buffer do we want? // how big of a buffer do we want?
if (length < JSON_BUFFER_SIZE) if (length < JSON_BUFFER_SIZE)
buffer_size = length+1; buffer_size = length + 1;
else else
buffer_size = JSON_BUFFER_SIZE; buffer_size = JSON_BUFFER_SIZE;
buffer = (char *)malloc(buffer_size); buffer = (char*)malloc(buffer_size);
if (buffer == NULL) { if (buffer == NULL) {
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); return error(HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
return ESP_FAIL;
} }
//send it in one shot or no? // send it in one shot or no?
if (length < JSON_BUFFER_SIZE) if (length < JSON_BUFFER_SIZE) {
{
serializeJson(_root, buffer, buffer_size); serializeJson(_root, buffer, buffer_size);
this->setContent((uint8_t *)buffer, length); setContent((uint8_t*)buffer, length);
this->setContentType(JSON_MIMETYPE); setContentType(JSON_MIMETYPE);
err = PsychicResponse::send(); err = send();
} } else {
else // helper class that acts as a stream to print chunked responses
{ ChunkPrinter dest(_response, (uint8_t*)buffer, buffer_size);
//helper class that acts as a stream to print chunked responses
ChunkPrinter dest(this, (uint8_t *)buffer, buffer_size);
//keep our headers // keep our headers
this->sendHeaders(); sendHeaders();
serializeJson(_root, dest); serializeJson(_root, dest);
//send the last bits // send the last bits
dest.flush(); dest.flush();
//done with our chunked response too // done with our chunked response too
err = this->finishChunking(); err = finishChunking();
} }
//let the buffer go // let the buffer go
free(buffer); free(buffer);
return err; return err;
} }
#ifdef ARDUINOJSON_6_COMPATIBILITY #ifdef ARDUINOJSON_6_COMPATIBILITY
PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) : PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) : _onRequest(NULL),
_onRequest(NULL), _maxJsonBufferSize(maxJsonBufferSize) {};
_maxJsonBufferSize(maxJsonBufferSize)
{};
PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) : PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) : _onRequest(onRequest),
_onRequest(onRequest), _maxJsonBufferSize(maxJsonBufferSize)
_maxJsonBufferSize(maxJsonBufferSize) {
{} }
#else #else
PsychicJsonHandler::PsychicJsonHandler() : PsychicJsonHandler::PsychicJsonHandler() : _onRequest(NULL) {};
_onRequest(NULL)
{};
PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) : PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) : _onRequest(onRequest)
_onRequest(onRequest) {
{} }
#endif #endif
void PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn) { _onRequest = fn; } void PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn)
esp_err_t PsychicJsonHandler::handleRequest(PsychicRequest *request)
{ {
//process basic stuff _onRequest = fn;
PsychicWebHandler::handleRequest(request); }
if (_onRequest) esp_err_t PsychicJsonHandler::handleRequest(PsychicRequest* request, PsychicResponse* response)
{ {
#ifdef ARDUINOJSON_6_COMPATIBILITY // process basic stuff
DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize); PsychicWebHandler::handleRequest(request, response);
DeserializationError error = deserializeJson(jsonBuffer, request->body());
if (error)
return request->reply(400);
JsonVariant json = jsonBuffer.as<JsonVariant>(); if (_onRequest) {
#else #ifdef ARDUINOJSON_6_COMPATIBILITY
JsonDocument jsonBuffer; DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, request->body()); DeserializationError error = deserializeJson(jsonBuffer, request->body());
if (error) if (error)
return request->reply(400); return response->send(400);
JsonVariant json = jsonBuffer.as<JsonVariant>(); JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif #else
JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, request->body());
if (error)
return response->send(400);
return _onRequest(request, json); JsonVariant json = jsonBuffer.as<JsonVariant>();
} #endif
else
return request->reply(500); return _onRequest(request, response, json);
} else
return response->send(500);
} }

View File

@@ -8,9 +8,9 @@
#ifndef PSYCHIC_JSON_H_ #ifndef PSYCHIC_JSON_H_
#define PSYCHIC_JSON_H_ #define PSYCHIC_JSON_H_
#include "ChunkPrinter.h"
#include "PsychicRequest.h" #include "PsychicRequest.h"
#include "PsychicWebHandler.h" #include "PsychicWebHandler.h"
#include "ChunkPrinter.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR == 6 #if ARDUINOJSON_VERSION_MAJOR == 6
@@ -20,70 +20,71 @@
#endif #endif
#endif #endif
#ifndef JSON_BUFFER_SIZE #ifndef JSON_BUFFER_SIZE
#define JSON_BUFFER_SIZE 4*1024 #define JSON_BUFFER_SIZE 4 * 1024
#endif #endif
constexpr const char *JSON_MIMETYPE = "application/json"; constexpr const char* JSON_MIMETYPE = "application/json";
/* /*
* Json Response * Json Response
* */ * */
class PsychicJsonResponse : public PsychicResponse class PsychicJsonResponse : public PsychicResponseDelegate
{ {
protected: protected:
#ifdef ARDUINOJSON_5_COMPATIBILITY #ifdef ARDUINOJSON_5_COMPATIBILITY
DynamicJsonBuffer _jsonBuffer; DynamicJsonBuffer _jsonBuffer;
#elif ARDUINOJSON_VERSION_MAJOR == 6 #elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument _jsonBuffer; DynamicJsonDocument _jsonBuffer;
#else #else
JsonDocument _jsonBuffer; JsonDocument _jsonBuffer;
#endif #endif
JsonVariant _root; JsonVariant _root;
size_t _contentLength; size_t _contentLength;
public: public:
#ifdef ARDUINOJSON_5_COMPATIBILITY #ifdef ARDUINOJSON_5_COMPATIBILITY
PsychicJsonResponse(PsychicRequest *request, bool isArray = false); PsychicJsonResponse(PsychicResponse* response, bool isArray = false);
#elif ARDUINOJSON_VERSION_MAJOR == 6 #elif ARDUINOJSON_VERSION_MAJOR == 6
PsychicJsonResponse(PsychicRequest *request, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); PsychicJsonResponse(PsychicResponse* response, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else #else
PsychicJsonResponse(PsychicRequest *request, bool isArray = false); PsychicJsonResponse(PsychicResponse* response, bool isArray = false);
#endif #endif
~PsychicJsonResponse() {} ~PsychicJsonResponse()
{
}
JsonVariant &getRoot(); JsonVariant& getRoot();
size_t getLength(); size_t getLength();
virtual esp_err_t send() override; esp_err_t send();
}; };
class PsychicJsonHandler : public PsychicWebHandler class PsychicJsonHandler : public PsychicWebHandler
{ {
protected: protected:
PsychicJsonRequestCallback _onRequest; PsychicJsonRequestCallback _onRequest;
#if ARDUINOJSON_VERSION_MAJOR == 6 #if ARDUINOJSON_VERSION_MAJOR == 6
const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE; const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE;
#endif #endif
public: public:
#ifdef ARDUINOJSON_5_COMPATIBILITY #ifdef ARDUINOJSON_5_COMPATIBILITY
PsychicJsonHandler(); PsychicJsonHandler();
PsychicJsonHandler(PsychicJsonRequestCallback onRequest); PsychicJsonHandler(PsychicJsonRequestCallback onRequest);
#elif ARDUINOJSON_VERSION_MAJOR == 6 #elif ARDUINOJSON_VERSION_MAJOR == 6
PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else #else
PsychicJsonHandler(); PsychicJsonHandler();
PsychicJsonHandler(PsychicJsonRequestCallback onRequest); PsychicJsonHandler(PsychicJsonRequestCallback onRequest);
#endif #endif
void onRequest(PsychicJsonRequestCallback fn); void onRequest(PsychicJsonRequestCallback fn);
virtual esp_err_t handleRequest(PsychicRequest *request) override; virtual esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override;
}; };
#endif #endif

View File

@@ -0,0 +1,6 @@
#include "PsychicMiddleware.h"
esp_err_t PsychicMiddlewareFunction::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)
{
return _fn(request, request->response(), next);
}

View File

@@ -0,0 +1,37 @@
#ifndef PsychicMiddleware_h
#define PsychicMiddleware_h
#include "PsychicCore.h"
#include "PsychicRequest.h"
#include "PsychicResponse.h"
class PsychicMiddlewareChain;
/*
* PsychicMiddleware :: fancy callback wrapper for handling requests and responses.
* */
class PsychicMiddleware
{
public:
virtual ~PsychicMiddleware() {}
virtual esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)
{
return next();
}
private:
friend PsychicMiddlewareChain;
bool _freeOnRemoval = false;
};
class PsychicMiddlewareFunction : public PsychicMiddleware
{
public:
PsychicMiddlewareFunction(PsychicMiddlewareCallback fn) : _fn(fn) { assert(_fn); }
esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override;
protected:
PsychicMiddlewareCallback _fn;
};
#endif

View File

@@ -0,0 +1,47 @@
#include "PsychicMiddlewareChain.h"
PsychicMiddlewareChain::~PsychicMiddlewareChain()
{
for (auto middleware : _middleware)
if (middleware->_freeOnRemoval)
delete middleware;
_middleware.clear();
}
void PsychicMiddlewareChain::addMiddleware(PsychicMiddleware* middleware)
{
_middleware.push_back(middleware);
}
void PsychicMiddlewareChain::addMiddleware(PsychicMiddlewareCallback fn)
{
PsychicMiddlewareFunction* closure = new PsychicMiddlewareFunction(fn);
closure->_freeOnRemoval = true;
_middleware.push_back(closure);
}
void PsychicMiddlewareChain::removeMiddleware(PsychicMiddleware* middleware)
{
_middleware.remove(middleware);
if (middleware->_freeOnRemoval)
delete middleware;
}
esp_err_t PsychicMiddlewareChain::runChain(PsychicRequest* request, PsychicMiddlewareNext finalizer)
{
if (_middleware.size() == 0)
return finalizer();
PsychicMiddlewareNext next;
std::list<PsychicMiddleware*>::iterator it = _middleware.begin();
next = [this, &next, &it, request, finalizer]() {
if (it == _middleware.end())
return finalizer();
PsychicMiddleware* m = *it;
it++;
return m->run(request, request->response(), next);
};
return next();
}

View File

@@ -0,0 +1,28 @@
#ifndef PsychicMiddlewareChain_h
#define PsychicMiddlewareChain_h
#include "PsychicCore.h"
#include "PsychicMiddleware.h"
#include "PsychicRequest.h"
#include "PsychicResponse.h"
/*
* PsychicMiddlewareChain - handle tracking and executing our chain of middleware objects
* */
class PsychicMiddlewareChain
{
public:
virtual ~PsychicMiddlewareChain();
void addMiddleware(PsychicMiddleware* middleware);
void addMiddleware(PsychicMiddlewareCallback fn);
void removeMiddleware(PsychicMiddleware* middleware);
esp_err_t runChain(PsychicRequest* request, PsychicMiddlewareNext finalizer);
protected:
std::list<PsychicMiddleware*> _middleware;
};
#endif

View File

@@ -0,0 +1,170 @@
#include "PsychicMiddlewares.h"
void LoggingMiddleware::setOutput(Print &output) {
_out = &output;
}
esp_err_t LoggingMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)
{
_out->print("* Connection from ");
_out->print(request->client()->remoteIP().toString());
_out->print(":");
_out->println(request->client()->remotePort());
_out->print("> ");
_out->print(request->methodStr());
_out->print(" ");
_out->print(request->uri());
_out->print(" ");
_out->println(request->version());
// TODO: find a way to collect all headers
// int n = request->headerCount();
// for (int i = 0; i < n; i++) {
// String v = server.header(i);
// if (!v.isEmpty()) {
// // because these 2 are always there, eventually empty: "Authorization", "If-None-Match"
// _out->print("< ");
// _out->print(server.headerName(i));
// _out->print(": ");
// _out->println(server.header(i));
// }
// }
_out->println(">");
esp_err_t ret = next();
if (ret != HTTPD_404_NOT_FOUND) {
_out->println("* Processed!");
_out->print("< ");
_out->print(response->version());
_out->print(" ");
_out->print(response->getCode());
_out->print(" ");
_out->println(http_status_reason(response->getCode()));
// iterate over response->headers()
std::list<HTTPHeader>::iterator it = response->headers().begin();
while (it != response->headers().end()) {
HTTPHeader h = *it;
_out->print("< ");
_out->print(h.field);
_out->print(": ");
_out->println(h.value);
it++;
}
_out->println("<");
} else {
_out->println("* Not processed!");
}
return ret;
}
AuthenticationMiddleware& AuthenticationMiddleware::setUsername(const char* username)
{
_username = username;
return *this;
}
AuthenticationMiddleware& AuthenticationMiddleware::setPassword(const char* password)
{
_password = password;
return *this;
}
AuthenticationMiddleware& AuthenticationMiddleware::setRealm(const char* realm)
{
_realm = realm;
return *this;
}
AuthenticationMiddleware& AuthenticationMiddleware::setAuthMethod(HTTPAuthMethod method)
{
_method = method;
return *this;
}
AuthenticationMiddleware& AuthenticationMiddleware::setAuthFailureMessage(const char* message)
{
_authFailMsg = message;
return *this;
}
bool AuthenticationMiddleware::isAllowed(PsychicRequest* request) const
{
if (!_username.isEmpty() && !_password.isEmpty()) {
return request->authenticate(_username.c_str(), _password.c_str());
}
return true;
}
esp_err_t AuthenticationMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)
{
bool authenticationRequired = false;
if (!_username.isEmpty() && !_password.isEmpty()) {
authenticationRequired = !request->authenticate(_username.c_str(), _password.c_str());
}
if (authenticationRequired) {
return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str());
} else {
return next();
}
}
CorsMiddleware& CorsMiddleware::setOrigin(const char* origin)
{
_origin = origin;
return *this;
}
CorsMiddleware& CorsMiddleware::setMethods(const char* methods)
{
_methods = methods;
return *this;
}
CorsMiddleware& CorsMiddleware::setHeaders(const char* headers)
{
_headers = headers;
return *this;
}
CorsMiddleware& CorsMiddleware::setAllowCredentials(bool credentials)
{
_credentials = credentials;
return *this;
}
CorsMiddleware& CorsMiddleware::setMaxAge(uint32_t seconds)
{
_maxAge = seconds;
return *this;
}
void CorsMiddleware::addCORSHeaders(PsychicResponse* response)
{
response->addHeader("Access-Control-Allow-Origin", _origin.c_str());
response->addHeader("Access-Control-Allow-Methods", _methods.c_str());
response->addHeader("Access-Control-Allow-Headers", _headers.c_str());
response->addHeader("Access-Control-Allow-Credentials", _credentials ? "true" : "false");
response->addHeader("Access-Control-Max-Age", String(_maxAge).c_str());
}
esp_err_t CorsMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next)
{
if (request->hasHeader("Origin")) {
addCORSHeaders(response);
if (request->method() == HTTP_OPTIONS) {
return response->send(200);
}
}
return next();
}

View File

@@ -0,0 +1,78 @@
#ifndef PsychicMiddlewares_h
#define PsychicMiddlewares_h
#include "PsychicMiddleware.h"
#include <Stream.h>
#include <http_status.h>
// curl-like logging middleware
class LoggingMiddleware : public PsychicMiddleware
{
public:
void setOutput(Print& output);
esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override;
private:
Print* _out;
};
class AuthenticationMiddleware : public PsychicMiddleware
{
public:
AuthenticationMiddleware& setUsername(const char* username);
AuthenticationMiddleware& setPassword(const char* password);
AuthenticationMiddleware& setRealm(const char* realm);
AuthenticationMiddleware& setAuthMethod(HTTPAuthMethod method);
AuthenticationMiddleware& setAuthFailureMessage(const char* message);
const String& getUsername() const { return _username; }
const String& getPassword() const { return _password; }
const String& getRealm() const { return _realm; }
HTTPAuthMethod getAuthMethod() const { return _method; }
const String& getAuthFailureMessage() const { return _authFailMsg; }
bool isAllowed(PsychicRequest* request) const;
esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override;
private:
String _username;
String _password;
String _realm;
HTTPAuthMethod _method = BASIC_AUTH;
String _authFailMsg;
};
class CorsMiddleware : public PsychicMiddleware
{
public:
CorsMiddleware& setOrigin(const char* origin);
CorsMiddleware& setMethods(const char* methods);
CorsMiddleware& setHeaders(const char* headers);
CorsMiddleware& setAllowCredentials(bool credentials);
CorsMiddleware& setMaxAge(uint32_t seconds);
const String& getOrigin() const { return _origin; }
const String& getMethods() const { return _methods; }
const String& getHeaders() const { return _headers; }
bool getAllowCredentials() const { return _credentials; }
uint32_t getMaxAge() const { return _maxAge; }
void addCORSHeaders(PsychicResponse* response);
esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override;
private:
String _origin = "*";
String _methods = "*";
String _headers = "*";
bool _credentials = true;
uint32_t _maxAge = 86400;
};
#endif

View File

@@ -1,91 +1,122 @@
#include "PsychicRequest.h" #include "PsychicRequest.h"
#include "http_status.h" #include "MultipartProcessor.h"
#include "PsychicHttpServer.h" #include "PsychicHttpServer.h"
#include "http_status.h"
PsychicRequest::PsychicRequest(PsychicHttpServer* server, httpd_req_t* req) : _server(server),
PsychicRequest::PsychicRequest(PsychicHttpServer *server, httpd_req_t *req) : _req(req),
_server(server), _endpoint(nullptr),
_req(req), _method(HTTP_GET),
_method(HTTP_GET), _uri(""),
_query(""), _query(""),
_body(""), _body(""),
_tempObject(NULL) _tempObject(nullptr)
{ {
//load up our client. // load up our client.
this->_client = server->getClient(req); this->_client = server->getClient(req);
//handle our session data // handle our session data
if (req->sess_ctx != NULL) if (req->sess_ctx != NULL)
this->_session = (SessionData *)req->sess_ctx; this->_session = (SessionData*)req->sess_ctx;
else else {
{
this->_session = new SessionData(); this->_session = new SessionData();
req->sess_ctx = this->_session; req->sess_ctx = this->_session;
} }
//callback for freeing the session later // callback for freeing the session later
req->free_ctx = this->freeSession; req->free_ctx = this->freeSession;
//load up some data // load and parse our uri.
this->_uri = String(this->_req->uri); this->_setUri(this->_req->uri);
_response = new PsychicResponse(this);
} }
PsychicRequest::~PsychicRequest() PsychicRequest::~PsychicRequest()
{ {
//temorary user object // temorary user object
if (_tempObject != NULL) if (_tempObject != NULL)
free(_tempObject); free(_tempObject);
//our web parameters // our web parameters
for (auto *param : _params) for (auto* param : _params)
delete(param); delete (param);
_params.clear(); _params.clear();
delete _response;
} }
void PsychicRequest::freeSession(void *ctx) void PsychicRequest::freeSession(void* ctx)
{ {
if (ctx != NULL) if (ctx != NULL) {
{ SessionData* session = (SessionData*)ctx;
SessionData *session = (SessionData*)ctx;
delete session; delete session;
} }
} }
PsychicHttpServer * PsychicRequest::server() { PsychicHttpServer* PsychicRequest::server()
{
return _server; return _server;
} }
httpd_req_t * PsychicRequest::request() { httpd_req_t* PsychicRequest::request()
{
return _req; return _req;
} }
PsychicClient * PsychicRequest::client() { PsychicClient* PsychicRequest::client()
{
return _client; return _client;
} }
PsychicEndpoint* PsychicRequest::endpoint()
{
return _endpoint;
}
void PsychicRequest::setEndpoint(PsychicEndpoint* endpoint)
{
_endpoint = endpoint;
}
#ifdef PSY_ENABLE_REGEX
bool PsychicRequest::getRegexMatches(std::smatch& matches, bool use_full_uri)
{
if (_endpoint != nullptr) {
std::regex pattern(_endpoint->uri().c_str());
std::string s(this->path().c_str());
if (use_full_uri)
s = this->uri().c_str();
return std::regex_search(s, matches, pattern);
}
return false;
}
#endif
const String PsychicRequest::getFilename() const String PsychicRequest::getFilename()
{ {
//parse the content-disposition header // parse the content-disposition header
if (this->hasHeader("Content-Disposition")) if (this->hasHeader("Content-Disposition")) {
{
ContentDisposition cd = this->getContentDisposition(); ContentDisposition cd = this->getContentDisposition();
if (cd.filename != "") if (cd.filename != "")
return cd.filename; return cd.filename;
} }
//fall back to passed in query string // fall back to passed in query string
PsychicWebParameter *param = getParam("_filename"); PsychicWebParameter* param = getParam("_filename");
if (param != NULL) if (param != NULL)
return param->name(); return param->name();
//fall back to parsing it from url (useful for wildcard uploads) // fall back to parsing it from url (useful for wildcard uploads)
String uri = this->uri(); String uri = this->uri();
int filenameStart = uri.lastIndexOf('/') + 1; int filenameStart = uri.lastIndexOf('/') + 1;
String filename = uri.substring(filenameStart); String filename = uri.substring(filenameStart);
if (filename != "") if (filename != "")
return filename; return filename;
//finally, unknown. // finally, unknown.
ESP_LOGE(PH_TAG, "Did not get a valid filename from the upload."); ESP_LOGE(PH_TAG, "Did not get a valid filename from the upload.");
return "unknown.txt"; return "unknown.txt";
} }
@@ -103,21 +134,19 @@ const ContentDisposition PsychicRequest::getContentDisposition()
cd.disposition = ATTACHMENT; cd.disposition = ATTACHMENT;
else if (header.indexOf("inline") == 0) else if (header.indexOf("inline") == 0)
cd.disposition = INLINE; cd.disposition = INLINE;
else else
cd.disposition = NONE; cd.disposition = NONE;
start = header.indexOf("filename="); start = header.indexOf("filename=");
if (start) if (start) {
{ end = header.indexOf('"', start + 10);
end = header.indexOf('"', start+10); cd.filename = header.substring(start + 10, end - 1);
cd.filename = header.substring(start+10, end-1);
} }
start = header.indexOf("name="); start = header.indexOf("name=");
if (start) if (start) {
{ end = header.indexOf('"', start + 6);
end = header.indexOf('"', start+6); cd.name = header.substring(start + 6, end - 1);
cd.name = header.substring(start+6, end-1);
} }
return cd; return cd;
@@ -125,16 +154,23 @@ const ContentDisposition PsychicRequest::getContentDisposition()
esp_err_t PsychicRequest::loadBody() esp_err_t PsychicRequest::loadBody()
{ {
esp_err_t err = ESP_OK; if (_bodyParsed != ESP_ERR_NOT_FINISHED)
return _bodyParsed;
// quick size check.
if (contentLength() > server()->maxRequestBodySize) {
ESP_LOGE(PH_TAG, "Body size larger than maxRequestBodySize");
return _bodyParsed = ESP_ERR_INVALID_SIZE;
}
this->_body = String(); this->_body = String();
size_t remaining = this->_req->content_len; size_t remaining = this->_req->content_len;
size_t actuallyReceived = 0; size_t actuallyReceived = 0;
char *buf = (char *)malloc(remaining + 1); char* buf = (char*)malloc(remaining + 1);
if (buf == NULL) { if (buf == NULL) {
ESP_LOGE(PH_TAG, "Failed to allocate memory for body"); ESP_LOGE(PH_TAG, "Failed to allocate memory for body");
return ESP_FAIL; return _bodyParsed = ESP_FAIL;
} }
while (remaining > 0) { while (remaining > 0) {
@@ -142,10 +178,9 @@ esp_err_t PsychicRequest::loadBody()
if (received == HTTPD_SOCK_ERR_TIMEOUT) { if (received == HTTPD_SOCK_ERR_TIMEOUT) {
continue; continue;
} } else if (received == HTTPD_SOCK_ERR_FAIL) {
else if (received == HTTPD_SOCK_ERR_FAIL) {
ESP_LOGE(PH_TAG, "Failed to receive data."); ESP_LOGE(PH_TAG, "Failed to receive data.");
err = ESP_FAIL; _bodyParsed = ESP_FAIL;
break; break;
} }
@@ -156,30 +191,38 @@ esp_err_t PsychicRequest::loadBody()
buf[actuallyReceived] = '\0'; buf[actuallyReceived] = '\0';
this->_body = String(buf); this->_body = String(buf);
free(buf); free(buf);
return err;
_bodyParsed = ESP_OK;
return _bodyParsed;
} }
http_method PsychicRequest::method() { http_method PsychicRequest::method()
{
return (http_method)this->_req->method; return (http_method)this->_req->method;
} }
const String PsychicRequest::methodStr() { const String PsychicRequest::methodStr()
{
return String(http_method_str((http_method)this->_req->method)); return String(http_method_str((http_method)this->_req->method));
} }
const String PsychicRequest::path() { const String PsychicRequest::path()
{
int index = _uri.indexOf("?"); int index = _uri.indexOf("?");
if(index == -1) if (index == -1)
return _uri; return _uri;
else else
return _uri.substring(0, index); return _uri.substring(0, index);
} }
const String& PsychicRequest::uri() { const String& PsychicRequest::uri()
{
return this->_uri; return this->_uri;
} }
const String& PsychicRequest::query() { const String& PsychicRequest::query()
{
return this->_query; return this->_query;
} }
@@ -188,35 +231,36 @@ const String& PsychicRequest::query() {
// { // {
// } // }
const String PsychicRequest::header(const char *name) const String PsychicRequest::header(const char* name)
{ {
size_t header_len = httpd_req_get_hdr_value_len(this->_req, name); size_t header_len = httpd_req_get_hdr_value_len(this->_req, name);
//if we've got one, allocated it and load it // if we've got one, allocated it and load it
if (header_len) if (header_len) {
{ char header[header_len + 1];
char header[header_len+1];
httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header)); httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header));
return String(header); return String(header);
} } else
else
return ""; return "";
} }
bool PsychicRequest::hasHeader(const char *name) bool PsychicRequest::hasHeader(const char* name)
{ {
return httpd_req_get_hdr_value_len(this->_req, name) > 0; return httpd_req_get_hdr_value_len(this->_req, name) > 0;
} }
const String PsychicRequest::host() { const String PsychicRequest::host()
{
return this->header("Host"); return this->header("Host");
} }
const String PsychicRequest::contentType() { const String PsychicRequest::contentType()
{
return header("Content-Type"); return header("Content-Type");
} }
size_t PsychicRequest::contentLength() { size_t PsychicRequest::contentLength()
{
return this->_req->content_len; return this->_req->content_len;
} }
@@ -232,90 +276,99 @@ bool PsychicRequest::isMultipart()
return (this->contentType().indexOf("multipart/form-data") >= 0); return (this->contentType().indexOf("multipart/form-data") >= 0);
} }
esp_err_t PsychicRequest::redirect(const char *url) bool PsychicRequest::hasCookie(const char* key, size_t* size)
{ {
PsychicResponse response(this); char buffer;
response.setCode(301);
response.addHeader("Location", url);
return response.send(); // this keeps our size for the user.
if (size != nullptr) {
*size = 1;
return getCookie(key, &buffer, size) != ESP_ERR_NOT_FOUND;
}
// this just checks that it exists.
else {
size_t mysize = 1;
return getCookie(key, &buffer, &mysize) != ESP_ERR_NOT_FOUND;
}
} }
bool PsychicRequest::hasCookie(const char *key) esp_err_t PsychicRequest::getCookie(const char* key, char* buffer, size_t* size)
{ {
char cookie[MAX_COOKIE_SIZE]; return httpd_req_get_cookie_val(this->_req, key, buffer, size);
size_t cookieSize = MAX_COOKIE_SIZE;
esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);
//did we get anything?
if (err == ESP_OK)
return true;
else if (err == ESP_ERR_HTTPD_RESULT_TRUNC)
ESP_LOGE(PH_TAG, "cookie too large (%d bytes).\n", cookieSize);
return false;
} }
const String PsychicRequest::getCookie(const char *key) String PsychicRequest::getCookie(const char* key)
{ {
char cookie[MAX_COOKIE_SIZE]; String cookie = "";
size_t cookieSize = MAX_COOKIE_SIZE;
esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);
//did we get anything? // how big is our cookie?
size_t size;
if (!hasCookie("counter", &size))
return cookie;
// allocate cookie buffer... keep it on the stack
char buf[size];
// load it up.
esp_err_t err = getCookie(key, buf, &size);
if (err == ESP_OK) if (err == ESP_OK)
return String(cookie); cookie.concat(buf);
else
return ""; return cookie;
}
void PsychicRequest::replaceResponse(PsychicResponse* response)
{
delete _response;
_response = response;
}
void PsychicRequest::addResponseHeader(const char* key, const char* value)
{
_response->addHeader(key, value);
}
std::list<HTTPHeader>& PsychicRequest::getResponseHeaders()
{
return _response->headers();
} }
void PsychicRequest::loadParams() void PsychicRequest::loadParams()
{ {
//did we get a query string? if (_paramsParsed != ESP_ERR_NOT_FINISHED)
size_t query_len = httpd_req_get_url_query_len(_req); return;
if (query_len)
{
char query[query_len+1];
httpd_req_get_url_query_str(_req, query, sizeof(query));
_query.concat(query);
//parse them. // convenience shortcut to allow calling loadParams()
if (_bodyParsed == ESP_ERR_NOT_FINISHED)
loadBody();
// various form data as parameters
if (this->method() == HTTP_POST) {
if (this->contentType().startsWith("application/x-www-form-urlencoded"))
_addParams(_body, true);
if (this->isMultipart()) {
MultipartProcessor mpp(this);
_paramsParsed = mpp.process(_body.c_str());
return;
}
}
_paramsParsed = ESP_OK;
}
void PsychicRequest::_setUri(const char* uri)
{
// save it
_uri = String(uri);
// look for our query separator
int index = _uri.indexOf('?', 0);
if (index) {
// parse them.
_query = _uri.substring(index + 1);
_addParams(_query, false); _addParams(_query, false);
} }
//did we get form data as body?
if (this->method() == HTTP_POST && this->contentType().startsWith("application/x-www-form-urlencoded"))
{
_addParams(_body, true);
}
}
void PsychicRequest::_addParams(const String& params, bool post){
size_t start = 0;
while (start < params.length()){
int end = params.indexOf('&', start);
if (end < 0) end = params.length();
int equal = params.indexOf('=', start);
if (equal < 0 || equal > end) equal = end;
String name = params.substring(start, equal);
String value = equal + 1 < end ? params.substring(equal + 1, end) : String();
addParam(name, value, true, post);
start = end + 1;
}
}
PsychicWebParameter * PsychicRequest::addParam(const String &name, const String &value, bool decode, bool post)
{
if (decode)
return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str()), post));
else
return addParam(new PsychicWebParameter(name, value, post));
}
PsychicWebParameter * PsychicRequest::addParam(PsychicWebParameter *param) {
// ESP_LOGD(PH_TAG, "Adding param: '%s' = '%s'", param->name().c_str(), param->value().c_str());
_params.push_back(param);
return param;
} }
int PsychicRequest::params() int PsychicRequest::params()
@@ -323,14 +376,51 @@ int PsychicRequest::params()
return _params.size(); return _params.size();
} }
bool PsychicRequest::hasParam(const char *key) void PsychicRequest::_addParams(const String& params, bool post)
{
size_t start = 0;
while (start < params.length()) {
int end = params.indexOf('&', start);
if (end < 0)
end = params.length();
int equal = params.indexOf('=', start);
if (equal < 0 || equal > end)
equal = end;
String name = params.substring(start, equal);
String value = equal + 1 < end ? params.substring(equal + 1, end) : String();
addParam(name, value, true, post);
start = end + 1;
}
}
PsychicWebParameter* PsychicRequest::addParam(const String& name, const String& value, bool decode, bool post)
{
if (decode)
return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str()), post));
else
return addParam(new PsychicWebParameter(name, value, post));
}
PsychicWebParameter* PsychicRequest::addParam(PsychicWebParameter* param)
{
// ESP_LOGD(PH_TAG, "Adding param: '%s' = '%s'", param->name().c_str(), param->value().c_str());
_params.push_back(param);
return param;
}
bool PsychicRequest::hasParam(const char* key)
{ {
return getParam(key) != NULL; return getParam(key) != NULL;
} }
PsychicWebParameter * PsychicRequest::getParam(const char *key) bool PsychicRequest::hasParam(const char* key, bool isPost, bool isFile)
{ {
for (auto *param : _params) return getParam(key, isPost, isFile) != NULL;
}
PsychicWebParameter* PsychicRequest::getParam(const char* key)
{
for (auto* param : _params)
if (param->name().equals(key)) if (param->name().equals(key))
return param; return param;
@@ -349,6 +439,14 @@ PsychicWebParameter * PsychicRequest::getParam(int index)
return NULL; return NULL;
} }
PsychicWebParameter* PsychicRequest::getParam(const char* key, bool isPost, bool isFile)
{
for (auto* param : _params)
if (param->name().equals(key) && isPost == param->isPost() && isFile == param->isFile())
return param;
return NULL;
}
bool PsychicRequest::hasSessionKey(const String& key) bool PsychicRequest::hasSessionKey(const String& key)
{ {
return this->_session->find(key) != this->_session->end(); return this->_session->find(key) != this->_session->end();
@@ -368,7 +466,8 @@ void PsychicRequest::setSessionKey(const String& key, const String& value)
this->_session->insert(std::pair<String, String>(key, value)); this->_session->insert(std::pair<String, String>(key, value));
} }
static const String md5str(const String &in){ static const String md5str(const String& in)
{
MD5Builder md5 = MD5Builder(); MD5Builder md5 = MD5Builder();
md5.begin(); md5.begin();
md5.add(in); md5.add(in);
@@ -376,28 +475,27 @@ static const String md5str(const String &in){
return md5.toString(); return md5.toString();
} }
bool PsychicRequest::authenticate(const char * username, const char * password) bool PsychicRequest::authenticate(const char* username, const char* password)
{ {
if(hasHeader("Authorization")) if (hasHeader("Authorization")) {
{
String authReq = header("Authorization"); String authReq = header("Authorization");
if(authReq.startsWith("Basic")){ if (authReq.startsWith("Basic")) {
authReq = authReq.substring(6); authReq = authReq.substring(6);
authReq.trim(); authReq.trim();
char toencodeLen = strlen(username)+strlen(password)+1; char toencodeLen = strlen(username) + strlen(password) + 1;
char *toencode = new char[toencodeLen + 1]; char* toencode = new char[toencodeLen + 1];
if(toencode == NULL){ if (toencode == NULL) {
authReq = ""; authReq = "";
return false; return false;
} }
char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; char* encoded = new char[base64_encode_expected_len(toencodeLen) + 1];
if(encoded == NULL){ if (encoded == NULL) {
authReq = ""; authReq = "";
delete[] toencode; delete[] toencode;
return false; return false;
} }
sprintf(toencode, "%s:%s", username, password); sprintf(toencode, "%s:%s", username, password);
if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) { if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) {
authReq = ""; authReq = "";
delete[] toencode; delete[] toencode;
delete[] encoded; delete[] encoded;
@@ -405,64 +503,62 @@ bool PsychicRequest::authenticate(const char * username, const char * password)
} }
delete[] toencode; delete[] toencode;
delete[] encoded; delete[] encoded;
} } else if (authReq.startsWith(F("Digest"))) {
else if(authReq.startsWith(F("Digest")))
{
authReq = authReq.substring(7); authReq = authReq.substring(7);
String _username = _extractParam(authReq,F("username=\""),'\"'); String _username = _extractParam(authReq, F("username=\""), '\"');
if(!_username.length() || _username != String(username)) { if (!_username.length() || _username != String(username)) {
authReq = ""; authReq = "";
return false; return false;
} }
// extracting required parameters for RFC 2069 simpler Digest // extracting required parameters for RFC 2069 simpler Digest
String _realm = _extractParam(authReq, F("realm=\""),'\"'); String _realm = _extractParam(authReq, F("realm=\""), '\"');
String _nonce = _extractParam(authReq, F("nonce=\""),'\"'); String _nonce = _extractParam(authReq, F("nonce=\""), '\"');
String _uri = _extractParam(authReq, F("uri=\""),'\"'); String _url = _extractParam(authReq, F("uri=\""), '\"');
String _resp = _extractParam(authReq, F("response=\""),'\"'); String _resp = _extractParam(authReq, F("response=\""), '\"');
String _opaque = _extractParam(authReq, F("opaque=\""),'\"'); String _opaque = _extractParam(authReq, F("opaque=\""), '\"');
if((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_resp.length()) || (!_opaque.length())) { if ((!_realm.length()) || (!_nonce.length()) || (!_url.length()) || (!_resp.length()) || (!_opaque.length())) {
authReq = ""; authReq = "";
return false; return false;
} }
if((_opaque != this->getSessionKey("opaque")) || (_nonce != this->getSessionKey("nonce")) || (_realm != this->getSessionKey("realm"))) if ((_opaque != this->getSessionKey("opaque")) || (_nonce != this->getSessionKey("nonce")) || (_realm != this->getSessionKey("realm"))) {
{
authReq = ""; authReq = "";
return false; return false;
} }
// parameters for the RFC 2617 newer Digest // parameters for the RFC 2617 newer Digest
String _nc,_cnonce; String _nc, _cnonce;
if(authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) { if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) {
_nc = _extractParam(authReq, F("nc="), ','); _nc = _extractParam(authReq, F("nc="), ',');
_cnonce = _extractParam(authReq, F("cnonce=\""),'\"'); _cnonce = _extractParam(authReq, F("cnonce=\""), '\"');
} }
String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password)); String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password));
//ESP_LOGD(PH_TAG, "Hash of user:realm:pass=%s", _H1.c_str()); // ESP_LOGD(PH_TAG, "Hash of user:realm:pass=%s", _H1.c_str());
String _H2 = ""; String _H2 = "";
if(_method == HTTP_GET){ http_method method = this->method();
_H2 = md5str(String(F("GET:")) + _uri); if (method == HTTP_GET) {
}else if(_method == HTTP_POST){ _H2 = md5str(String(F("GET:")) + _url);
_H2 = md5str(String(F("POST:")) + _uri); } else if (method == HTTP_POST) {
}else if(_method == HTTP_PUT){ _H2 = md5str(String(F("POST:")) + _url);
_H2 = md5str(String(F("PUT:")) + _uri); } else if (method == HTTP_PUT) {
}else if(_method == HTTP_DELETE){ _H2 = md5str(String(F("PUT:")) + _url);
_H2 = md5str(String(F("DELETE:")) + _uri); } else if (method == HTTP_DELETE) {
}else{ _H2 = md5str(String(F("DELETE:")) + _url);
_H2 = md5str(String(F("GET:")) + _uri);
}
//ESP_LOGD(PH_TAG, "Hash of GET:uri=%s", _H2.c_str());
String _responsecheck = "";
if(authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) {
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
} else { } else {
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); _H2 = md5str(String(F("GET:")) + _url);
} }
// ESP_LOGD(PH_TAG, "Hash of GET:uri=%s", _H2.c_str());
//ESP_LOGD(PH_TAG, "The Proper response=%s", _responsecheck.c_str());
if(_resp == _responsecheck){ String _responsecheck = "";
if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) {
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
} else {
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2);
}
// ESP_LOGD(PH_TAG, "The Proper response=%s", _responsecheck.c_str());
if (_resp == _responsecheck) {
authReq = ""; authReq = "";
return true; return true;
} }
@@ -477,23 +573,23 @@ const String PsychicRequest::_extractParam(const String& authReq, const String&
int _begin = authReq.indexOf(param); int _begin = authReq.indexOf(param);
if (_begin == -1) if (_begin == -1)
return ""; return "";
return authReq.substring(_begin+param.length(),authReq.indexOf(delimit,_begin+param.length())); return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
} }
const String PsychicRequest::_getRandomHexString() const String PsychicRequest::_getRandomHexString()
{ {
char buffer[33]; // buffer to hold 32 Hex Digit + /0 char buffer[33]; // buffer to hold 32 Hex Digit + /0
int i; int i;
for(i = 0; i < 4; i++) { for (i = 0; i < 4; i++) {
sprintf (buffer + (i*8), "%08lx", (unsigned long int)esp_random()); sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random());
} }
return String(buffer); return String(buffer);
} }
esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg) esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg)
{ {
//what is thy realm, sire? // what is thy realm, sire?
if(!strcmp(realm, "")) if (!strcmp(realm, ""))
this->setSessionKey("realm", "Login Required"); this->setSessionKey("realm", "Login Required");
else else
this->setSessionKey("realm", realm); this->setSessionKey("realm", realm);
@@ -501,17 +597,14 @@ esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char*
PsychicResponse response(this); PsychicResponse response(this);
String authStr; String authStr;
//what kind of auth? // what kind of auth?
if(mode == BASIC_AUTH) if (mode == BASIC_AUTH) {
{
authStr = "Basic realm=\"" + this->getSessionKey("realm") + "\""; authStr = "Basic realm=\"" + this->getSessionKey("realm") + "\"";
response.addHeader("WWW-Authenticate", authStr.c_str()); response.addHeader("WWW-Authenticate", authStr.c_str());
} } else {
else // only make new ones if we havent sent them yet
{
//only make new ones if we havent sent them yet
if (this->getSessionKey("nonce").isEmpty()) if (this->getSessionKey("nonce").isEmpty())
this->setSessionKey("nonce", _getRandomHexString()); this->setSessionKey("nonce", _getRandomHexString());
if (this->getSessionKey("opaque").isEmpty()) if (this->getSessionKey("opaque").isEmpty())
this->setSessionKey("opaque", _getRandomHexString()); this->setSessionKey("opaque", _getRandomHexString());
@@ -521,39 +614,6 @@ esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char*
response.setCode(401); response.setCode(401);
response.setContentType("text/html"); response.setContentType("text/html");
response.setContent(authStr.c_str()); response.setContent(authFailMsg);
return response.send(); return response.send();
} }
esp_err_t PsychicRequest::reply(int code)
{
PsychicResponse response(this);
response.setCode(code);
response.setContentType("text/plain");
response.setContent(http_status_reason(code));
return response.send();
}
esp_err_t PsychicRequest::reply(const char *content)
{
PsychicResponse response(this);
response.setCode(200);
response.setContentType("text/html");
response.setContent(content);
return response.send();
}
esp_err_t PsychicRequest::reply(int code, const char *contentType, const char *content)
{
PsychicResponse response(this);
response.setCode(code);
response.setContentType(contentType);
response.setContent(content);
return response.send();
}

View File

@@ -1,38 +1,55 @@
#ifndef PsychicRequest_h #ifndef PsychicRequest_h
#define PsychicRequest_h #define PsychicRequest_h
#include "PsychicCore.h"
#include "PsychicHttpServer.h"
#include "PsychicClient.h" #include "PsychicClient.h"
#include "PsychicCore.h"
#include "PsychicEndpoint.h"
#include "PsychicHttpServer.h"
#include "PsychicWebParameter.h" #include "PsychicWebParameter.h"
#include "PsychicResponse.h"
#ifdef PSY_ENABLE_REGEX
#include <regex>
#endif
typedef std::map<String, String> SessionData; typedef std::map<String, String> SessionData;
enum Disposition { NONE, INLINE, ATTACHMENT, FORM_DATA}; enum Disposition {
NONE,
struct ContentDisposition { INLINE,
Disposition disposition; ATTACHMENT,
String filename; FORM_DATA
String name;
}; };
class PsychicRequest { struct ContentDisposition {
friend PsychicHttpServer; Disposition disposition;
String filename;
String name;
};
class PsychicRequest
{
friend PsychicHttpServer;
friend PsychicResponse;
protected: protected:
PsychicHttpServer *_server; PsychicHttpServer* _server;
httpd_req_t *_req; httpd_req_t* _req;
SessionData *_session; SessionData* _session;
PsychicClient *_client; PsychicClient* _client;
PsychicEndpoint* _endpoint;
http_method _method; http_method _method;
String _uri; String _uri;
String _query; String _query;
String _body; String _body;
esp_err_t _bodyParsed = ESP_ERR_NOT_FINISHED;
esp_err_t _paramsParsed = ESP_ERR_NOT_FINISHED;
std::list<PsychicWebParameter*> _params; std::list<PsychicWebParameter*> _params;
PsychicResponse* _response;
void _setUri(const char* uri);
void _addParams(const String& params, bool post); void _addParams(const String& params, bool post);
void _parseGETParams(); void _parseGETParams();
void _parsePOSTParams(); void _parsePOSTParams();
@@ -41,28 +58,59 @@ class PsychicRequest {
const String _getRandomHexString(); const String _getRandomHexString();
public: public:
PsychicRequest(PsychicHttpServer *server, httpd_req_t *req); PsychicRequest(PsychicHttpServer* server, httpd_req_t* req);
virtual ~PsychicRequest(); virtual ~PsychicRequest();
void *_tempObject; void* _tempObject;
PsychicHttpServer * server(); PsychicHttpServer* server();
httpd_req_t * request(); httpd_req_t* request();
virtual PsychicClient * client(); virtual PsychicClient* client();
PsychicEndpoint* endpoint();
void setEndpoint(PsychicEndpoint* endpoint);
#ifdef PSY_ENABLE_REGEX
bool getRegexMatches(std::smatch& matches, bool use_full_uri = false);
#endif
bool isMultipart(); bool isMultipart();
esp_err_t loadBody(); esp_err_t loadBody();
const String header(const char *name); const String header(const char* name);
bool hasHeader(const char *name); bool hasHeader(const char* name);
static void freeSession(void *ctx); static void freeSession(void* ctx);
bool hasSessionKey(const String& key); bool hasSessionKey(const String& key);
const String getSessionKey(const String& key); const String getSessionKey(const String& key);
void setSessionKey(const String& key, const String& value); void setSessionKey(const String& key, const String& value);
bool hasCookie(const char * key); bool hasCookie(const char* key, size_t* size = nullptr);
const String getCookie(const char * key);
PsychicResponse* response() { return _response; }
void replaceResponse(PsychicResponse* response);
void addResponseHeader(const char* key, const char* value);
std::list<HTTPHeader>& getResponseHeaders();
/**
* @brief Get the value string of a cookie value from the "Cookie" request headers by cookie name.
*
* @param[in] key The cookie name to be searched in the request
* @param[out] buffer Pointer to the buffer into which the value of cookie will be copied if the cookie is found
* @param[inout] size Pointer to size of the user buffer "val". This variable will contain cookie length if
* ESP_OK is returned and required buffer length in case ESP_ERR_HTTPD_RESULT_TRUNC is returned.
*
* @return
* - ESP_OK : Key is found in the cookie string and copied to buffer. The value is null-terminated.
* - ESP_ERR_NOT_FOUND : Key not found
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated
* - ESP_ERR_NO_MEM : Memory allocation failure
*/
esp_err_t getCookie(const char* key, char* buffer, size_t* size);
// convenience / lazy function for getting cookies.
String getCookie(const char* key);
http_method method(); // returns the HTTP method used as enum value (eg. HTTP_GET) http_method method(); // returns the HTTP method used as enum value (eg. HTTP_GET)
const String methodStr(); // returns the HTTP method used as a string (eg. "GET") const String methodStr(); // returns the HTTP method used as a string (eg. "GET")
@@ -74,27 +122,25 @@ class PsychicRequest {
size_t contentLength(); // returns the Content-Length header value size_t contentLength(); // returns the Content-Length header value
const String& body(); // returns the body of the request const String& body(); // returns the body of the request
const ContentDisposition getContentDisposition(); const ContentDisposition getContentDisposition();
const char* version() { return "HTTP/1.1"; }
const String& queryString() { return query(); } //compatability function. same as query() const String& queryString() { return query(); } // compatability function. same as query()
const String& url() { return uri(); } //compatability function. same as uri() const String& url() { return uri(); } // compatability function. same as uri()
void loadParams(); void loadParams();
PsychicWebParameter * addParam(PsychicWebParameter *param); PsychicWebParameter* addParam(PsychicWebParameter* param);
PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true, bool post = false); PsychicWebParameter* addParam(const String& name, const String& value, bool decode = true, bool post = false);
int params(); int params();
bool hasParam(const char *key); bool hasParam(const char* key);
PsychicWebParameter * getParam(const char *name); bool hasParam(const char* key, bool isPost, bool isFile = false);
PsychicWebParameter * getParam(int index); PsychicWebParameter* getParam(const char* name);
PsychicWebParameter* getParam(int index);
PsychicWebParameter* getParam(const char* name, bool isPost, bool isFile = false);
const String getFilename(); const String getFilename();
bool authenticate(const char * username, const char * password); bool authenticate(const char* username, const char* password);
esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg); esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg);
esp_err_t redirect(const char *url);
esp_err_t reply(int code);
esp_err_t reply(const char *content);
esp_err_t reply(int code, const char *contentType, const char *content);
}; };
#endif // PsychicRequest_h #endif // PsychicRequest_h

View File

@@ -2,52 +2,49 @@
#include "PsychicRequest.h" #include "PsychicRequest.h"
#include <http_status.h> #include <http_status.h>
PsychicResponse::PsychicResponse(PsychicRequest *request) : PsychicResponse::PsychicResponse(PsychicRequest* request) : _request(request),
_request(request), _code(200),
_code(200), _status(""),
_status(""), _contentType(emptyString),
_contentLength(0), _contentLength(0),
_body("") _body("")
{ {
// get our global headers out of the way
for (auto& header : DefaultHeaders::Instance().getHeaders())
addHeader(header.field.c_str(), header.value.c_str());
} }
PsychicResponse::~PsychicResponse() PsychicResponse::~PsychicResponse()
{ {
//clean up our header variables. we have to do this on desctruct since httpd_resp_send doesn't store copies
for (HTTPHeader header : _headers)
{
free(header.field);
free(header.value);
}
_headers.clear(); _headers.clear();
} }
void PsychicResponse::addHeader(const char *field, const char *value) void PsychicResponse::addHeader(const char* field, const char* value)
{ {
//these get freed after send by the destructor // erase any existing ones.
HTTPHeader header; for (auto itr = _headers.begin(); itr != _headers.end();) {
header.field =(char *)malloc(strlen(field)+1); if (itr->field.equalsIgnoreCase(field))
header.value = (char *)malloc(strlen(value)+1); itr = _headers.erase(itr);
else
itr++;
}
strlcpy(header.field, field, strlen(field)+1); // now add it.
strlcpy(header.value, value, strlen(value)+1); _headers.push_back({field, value});
_headers.push_back(header);
} }
void PsychicResponse::setCookie(const char *name, const char *value, unsigned long secondsFromNow, const char *extras) void PsychicResponse::setCookie(const char* name, const char* value, unsigned long secondsFromNow, const char* extras)
{ {
time_t now = time(nullptr); time_t now = time(nullptr);
String output; String output;
output = urlEncode(name) + "=" + urlEncode(value); output = urlEncode(name) + "=" + urlEncode(value);
//if current time isn't modern, default to using max age // if current time isn't modern, default to using max age
if (now < 1700000000) if (now < 1700000000)
output += "; Max-Age=" + String(secondsFromNow); output += "; Max-Age=" + String(secondsFromNow);
//otherwise, set an expiration date // otherwise, set an expiration date
else else {
{
time_t expirationTimestamp = now + secondsFromNow; time_t expirationTimestamp = now + secondsFromNow;
// Convert the expiration timestamp to a formatted string for the "expires" attribute // Convert the expiration timestamp to a formatted string for the "expires" attribute
@@ -57,11 +54,11 @@ void PsychicResponse::setCookie(const char *name, const char *value, unsigned lo
output += "; Expires=" + String(expires); output += "; Expires=" + String(expires);
} }
//did we get any extras? // did we get any extras?
if (strlen(extras)) if (strlen(extras))
output += "; " + String(extras); output += "; " + String(extras);
//okay, add it in. // okay, add it in.
addHeader("Set-Cookie", output.c_str()); addHeader("Set-Cookie", output.c_str());
} }
@@ -70,24 +67,24 @@ void PsychicResponse::setCode(int code)
_code = code; _code = code;
} }
void PsychicResponse::setContentType(const char *contentType) void PsychicResponse::setContentType(const char* contentType)
{ {
httpd_resp_set_type(_request->request(), contentType); _contentType = contentType;
} }
void PsychicResponse::setContent(const char *content) void PsychicResponse::setContent(const char* content)
{ {
_body = content; _body = content;
setContentLength(strlen(content)); setContentLength(strlen(content));
} }
void PsychicResponse::setContent(const uint8_t *content, size_t len) void PsychicResponse::setContent(const uint8_t* content, size_t len)
{ {
_body = (char *)content; _body = (char*)content;
setContentLength(len); setContentLength(len);
} }
const char * PsychicResponse::getContent() const char* PsychicResponse::getContent()
{ {
return _body; return _body;
} }
@@ -99,17 +96,20 @@ size_t PsychicResponse::getContentLength()
esp_err_t PsychicResponse::send() esp_err_t PsychicResponse::send()
{ {
//esp-idf makes you set the whole status. // esp-idf makes you set the whole status.
sprintf(_status, "%u %s", _code, http_status_reason(_code)); sprintf(_status, "%u %s", _code, http_status_reason(_code));
httpd_resp_set_status(_request->request(), _status); httpd_resp_set_status(_request->request(), _status);
//our headers too // set the content type
httpd_resp_set_type(_request->request(), _contentType.c_str());
// our headers too
this->sendHeaders(); this->sendHeaders();
//now send it off // now send it off
esp_err_t err = httpd_resp_send(_request->request(), getContent(), getContentLength()); esp_err_t err = httpd_resp_send(_request->request(), getContent(), getContentLength());
//did something happen? // did something happen?
if (err != ESP_OK) if (err != ESP_OK)
ESP_LOGE(PH_TAG, "Send response failed (%s)", esp_err_to_name(err)); ESP_LOGE(PH_TAG, "Send response failed (%s)", esp_err_to_name(err));
@@ -118,38 +118,21 @@ esp_err_t PsychicResponse::send()
void PsychicResponse::sendHeaders() void PsychicResponse::sendHeaders()
{ {
//get our global headers out of the way first // now do our individual headers
for (HTTPHeader header : DefaultHeaders::Instance().getHeaders()) for (auto& header : _headers)
httpd_resp_set_hdr(_request->request(), header.field, header.value); httpd_resp_set_hdr(this->_request->request(), header.field.c_str(), header.value.c_str());
//now do our individual headers
for (HTTPHeader header : _headers)
httpd_resp_set_hdr(this->_request->request(), header.field, header.value);
// DO NOT RELEASE HEADERS HERE... released in the PsychicResponse destructor after they have been sent.
// httpd_resp_set_hdr just passes on the pointer, but its needed after this call.
// clean up our header variables after send
// for (HTTPHeader header : _headers)
// {
// free(header.field);
// free(header.value);
// }
// _headers.clear();
} }
esp_err_t PsychicResponse::sendChunk(uint8_t *chunk, size_t chunksize) esp_err_t PsychicResponse::sendChunk(uint8_t* chunk, size_t chunksize)
{ {
/* Send the buffer contents as HTTP response chunk */ /* Send the buffer contents as HTTP response chunk */
esp_err_t err = httpd_resp_send_chunk(this->_request->request(), (char *)chunk, chunksize); ESP_LOGD(PH_TAG, "Sending chunk: %d", chunksize);
if (err != ESP_OK) esp_err_t err = httpd_resp_send_chunk(request(), (char*)chunk, chunksize);
{ if (err != ESP_OK) {
ESP_LOGE(PH_TAG, "File sending failed (%s)", esp_err_to_name(err)); ESP_LOGE(PH_TAG, "File sending failed (%s)", esp_err_to_name(err));
/* Abort sending file */ /* Abort sending file */
httpd_resp_sendstr_chunk(this->_request->request(), NULL); httpd_resp_sendstr_chunk(this->_request->request(), NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
} }
return err; return err;
@@ -159,4 +142,63 @@ esp_err_t PsychicResponse::finishChunking()
{ {
/* Respond with an empty chunk to signal HTTP response completion */ /* Respond with an empty chunk to signal HTTP response completion */
return httpd_resp_send_chunk(this->_request->request(), NULL, 0); return httpd_resp_send_chunk(this->_request->request(), NULL, 0);
} }
esp_err_t PsychicResponse::redirect(const char* url)
{
if (!_code)
setCode(301);
addHeader("Location", url);
return send();
}
esp_err_t PsychicResponse::send(int code)
{
setCode(code);
return send();
}
esp_err_t PsychicResponse::send(const char* content)
{
if (!_code)
setCode(200);
if (_contentType.isEmpty())
setContentType("text/html");
setContent(content);
return send();
}
esp_err_t PsychicResponse::send(const char* contentType, const char* content)
{
if (!_code)
setCode(200);
setContentType(contentType);
setContent(content);
return send();
}
esp_err_t PsychicResponse::send(int code, const char* contentType, const char* content)
{
setCode(code);
setContentType(contentType);
setContent(content);
return send();
}
esp_err_t PsychicResponse::send(int code, const char* contentType, const uint8_t* content, size_t len)
{
setCode(code);
setContentType(contentType);
setContent(content, len);
return send();
}
esp_err_t PsychicResponse::error(httpd_err_code_t code, const char* message)
{
return httpd_resp_send_err(_request->_req, code, message);
}
httpd_req_t* PsychicResponse::request()
{
return _request->_req;
}

View File

@@ -9,38 +9,101 @@ class PsychicRequest;
class PsychicResponse class PsychicResponse
{ {
protected: protected:
PsychicRequest *_request; PsychicRequest* _request;
int _code; int _code;
char _status[60]; char _status[60];
std::list<HTTPHeader> _headers; std::list<HTTPHeader> _headers;
String _contentType;
int64_t _contentLength; int64_t _contentLength;
const char * _body; const char* _body;
public: public:
PsychicResponse(PsychicRequest *request); PsychicResponse(PsychicRequest* request);
virtual ~PsychicResponse(); virtual ~PsychicResponse();
void setCode(int code); const char* version() { return "HTTP/1.1"; }
void setCode(int code);
int getCode() { return _code; }
void setContentType(const char* contentType);
String& getContentType() { return _contentType; }
void setContentType(const char *contentType);
void setContentLength(int64_t contentLength) { _contentLength = contentLength; } void setContentLength(int64_t contentLength) { _contentLength = contentLength; }
int64_t getContentLength(int64_t contentLength) { return _contentLength; } int64_t getContentLength(int64_t contentLength) { return _contentLength; }
void addHeader(const char *field, const char *value); void addHeader(const char* field, const char* value);
std::list<HTTPHeader>& headers() { return _headers; }
void setCookie(const char *key, const char *value, unsigned long max_age = 60*60*24*30, const char *extras = ""); void setCookie(const char* key, const char* value, unsigned long max_age = 60 * 60 * 24 * 30, const char* extras = "");
void setContent(const char *content); void setContent(const char* content);
void setContent(const uint8_t *content, size_t len); void setContent(const uint8_t* content, size_t len);
const char * getContent(); const char* getContent();
size_t getContentLength(); size_t getContentLength();
virtual esp_err_t send(); virtual esp_err_t send();
void sendHeaders(); void sendHeaders();
esp_err_t sendChunk(uint8_t *chunk, size_t chunksize); esp_err_t sendChunk(uint8_t* chunk, size_t chunksize);
esp_err_t finishChunking(); esp_err_t finishChunking();
esp_err_t redirect(const char* url);
esp_err_t send(int code);
esp_err_t send(const char* content);
esp_err_t send(const char* contentType, const char* content);
esp_err_t send(int code, const char* contentType, const char* content);
esp_err_t send(int code, const char* contentType, const uint8_t* content, size_t len);
esp_err_t error(httpd_err_code_t code, const char* message);
httpd_req_t* request();
};
class PsychicResponseDelegate
{
protected:
PsychicResponse* _response;
public:
PsychicResponseDelegate(PsychicResponse* response) : _response(response) {}
virtual ~PsychicResponseDelegate() {}
const char* version() { return _response->version(); }
void setCode(int code) { _response->setCode(code); }
void setContentType(const char* contentType) { _response->setContentType(contentType); }
String& getContentType() { return _response->getContentType(); }
void setContentLength(int64_t contentLength) { _response->setContentLength(contentLength); }
int64_t getContentLength(int64_t contentLength) { return _response->getContentLength(); }
void addHeader(const char* field, const char* value) { _response->addHeader(field, value); }
void setCookie(const char* key, const char* value, unsigned long max_age = 60 * 60 * 24 * 30, const char* extras = "") { _response->setCookie(key, value, max_age, extras); }
void setContent(const char* content) { _response->setContent(content); }
void setContent(const uint8_t* content, size_t len) { _response->setContent(content, len); }
const char* getContent() { return _response->getContent(); }
size_t getContentLength() { return _response->getContentLength(); }
esp_err_t send() { return _response->send(); }
void sendHeaders() { _response->sendHeaders(); }
esp_err_t sendChunk(uint8_t* chunk, size_t chunksize) { return _response->sendChunk(chunk, chunksize); }
esp_err_t finishChunking() { return _response->finishChunking(); }
esp_err_t redirect(const char* url) { return _response->redirect(url); }
esp_err_t send(int code) { return _response->send(code); }
esp_err_t send(const char* content) { return _response->send(content); }
esp_err_t send(const char* contentType, const char* content) { return _response->send(contentType, content); }
esp_err_t send(int code, const char* contentType, const char* content) { return _response->send(code, contentType, content); }
esp_err_t send(int code, const char* contentType, const uint8_t* content, size_t len) { return _response->send(code, contentType, content, len); }
esp_err_t error(httpd_err_code_t code, const char* message) { return _response->error(code, message); }
httpd_req_t* request() { return _response->request(); }
}; };
#endif // PsychicResponse_h #endif // PsychicResponse_h

View File

@@ -0,0 +1,54 @@
#include "PsychicRewrite.h"
#include "PsychicRequest.h"
PsychicRewrite::PsychicRewrite(const char* from, const char* to):
_fromPath(from),
_toUri(to),
_toPath(String()),
_toParams(String()),
_filter(nullptr)
{
int index = _toUri.indexOf('?');
if (index > 0) {
_toParams = _toUri.substring(index + 1);
_toPath = _toUri.substring(0, index);
}
else
_toPath = _toUri;
}
PsychicRewrite::~PsychicRewrite()
{
}
PsychicRewrite* PsychicRewrite::setFilter(PsychicRequestFilterFunction fn)
{
_filter = fn; return this;
}
bool PsychicRewrite::filter(PsychicRequest *request) const
{
return _filter == nullptr || _filter(request);
}
const String& PsychicRewrite::from(void) const
{
return _fromPath;
}
const String& PsychicRewrite::toUrl(void) const
{
return _toUri;
}
const String& PsychicRewrite::params(void) const
{
return _toParams;
}
bool PsychicRewrite::match(PsychicRequest *request)
{
if (!filter(request))
return false;
return _fromPath == request->path();
}

Some files were not shown because too many files have changed in this diff Show More