esp32-sk6812/src/WebServer.cpp

391 lines
11 KiB
C++

/* HTTP Server setup and handler functions */
#include <SPIFFS.h>
#include <mbedtls/sha256.h>
#include "WebServer.h"
#include "Config.h"
#include "Fader.h"
#include "Font.h"
#include "Animation/AllAnimations.h"
#include "Animation/AnimationController.h"
#include "coreids.h"
WebServer::WebServer(void)
: m_cr(Config::instance().getCRPassword()), m_fader(NULL)
{
m_server = new httpsserver::HTTPServer();
}
bool WebServer::serveFile(String filename, httpsserver::HTTPResponse *res)
{
uint8_t buf[1024];
xSemaphoreTake(*instance().m_ledLockoutMutex, portMAX_DELAY);
bool exists = SPIFFS.exists(filename);
xSemaphoreGive(*instance().m_ledLockoutMutex);
if(!exists) {
return false;
}
xSemaphoreTake(*instance().m_ledLockoutMutex, portMAX_DELAY);
File f = SPIFFS.open(filename.c_str(), "r");
xSemaphoreGive(*instance().m_ledLockoutMutex);
if(!f) {
return false;
}
res->setHeader("Content-Length", httpsserver::intToString(f.size()));
size_t nread = 1;
while(nread > 0) {
xSemaphoreTake(*instance().m_ledLockoutMutex, portMAX_DELAY);
nread = f.readBytes(reinterpret_cast<char*>(buf), 1024);
xSemaphoreGive(*instance().m_ledLockoutMutex);
if(nread <= 0) {
break;
}
res->write(buf, nread);
}
xSemaphoreTake(*instance().m_ledLockoutMutex, portMAX_DELAY);
f.close();
xSemaphoreGive(*instance().m_ledLockoutMutex);
return true;
}
void WebServer::handleRoot(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
serveFile("/htdocs/index.html", res);
res->setHeader("Content-Type", "text/html");
}
static void error400(httpsserver::HTTPResponse *res, const std::string &reason)
{
res->println("Error 400: Bad Request");
res->println(reason.c_str());
res->setStatusCode(400);
}
static bool parseColor(const std::string &colorstr, Fader::Color *color, std::string *error)
{
if(colorstr.length() != 8) {
*error = "Wrong length of color string (expected 8 hex digits).";
return false;
}
uint32_t colorval = 0;
for(size_t i = 0; i < 8; i++) {
colorval <<= 4;
char c = colorstr[i];
if(c >= 'A' && c <= 'F') {
colorval |= c - 'A' + 10;
} else if(c >= 'a' && c <= 'f') {
colorval |= c - 'a' + 10;
} else if(c >= '0' && c <= '9') {
colorval |= c - '0';
} else {
*error = "Invalid character in color string (allowed: 0-9, a-f, A-F)";
return false;
}
}
color->r = (colorval & 0xFF000000) >> 24;
color->g = (colorval & 0x00FF0000) >> 16;
color->b = (colorval & 0x0000FF00) >> 8;
color->w = (colorval & 0x000000FF) >> 0;
return true;
}
void WebServer::handleColor(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "text/plain");
httpsserver::ResourceParameters * params = req->getParams();
std::string colorstr;
if(!params->getQueryParameter("color", colorstr)) {
error400(res, "Parameter 'color' not set.");
return;
}
Fader::Color color;
std::string errorMessage;
if(!parseColor(colorstr, &color, &errorMessage)) {
error400(res, errorMessage);
return;
}
res->print("Color changed to r:");
res->print(color.r);
res->print(" g:");
res->print(color.g);
res->print(" b:");
res->print(color.b);
res->print(" w:");
res->println(color.w);
WebServer::instance().m_animController->changeAnimation(
std::unique_ptr<Animation>(
new FadeToColorAnimation(WebServer::instance().m_fader, color)),
false);
}
void WebServer::handleSetAnim(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "text/plain");
httpsserver::ResourceParameters * params = req->getParams();
std::string animstr;
if(!params->getQueryParameter("anim", animstr)) {
error400(res, "Parameter 'anim' not set.");
return;
}
std::istringstream animstream(animstr);
uint32_t anim_id;
animstream >> anim_id;
if(animstream.bad() || animstream.fail()) {
error400(res, "Parameter 'anim' could not be parsed as number.");
return;
}
if(anim_id >= AnimationController::NUM_DEFAULT_ANIMATIONS) {
error400(res, "Animation ID out of range.");
return;
}
WebServer::instance().m_animController->changeAnimation(
static_cast<AnimationController::DefaultAnimation>(anim_id),
true,
AnimationController::USER);
}
void WebServer::handleListAnim(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
res->print("[");
for(size_t i = 0; i < AnimationController::AnimationNames.size(); i++) {
res->print("{\"id\":");
res->print(i);
res->print(",\"name\":\"");
res->print(AnimationController::AnimationNames[i]);
res->print("\"}");
if(i < AnimationController::AnimationNames.size()-1) {
res->print(",");
}
}
res->print("]");
}
void WebServer::handleText(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "text/plain");
httpsserver::ResourceParameters * params = req->getParams();
Fader::Color color;
std::string colorstr;
if(params->getQueryParameter("color", colorstr)) {
std::string errorMessage;
if(!parseColor(colorstr, &color, &errorMessage)) {
error400(res, errorMessage);
return;
}
} else {
color = Fader::Color{0, 0, 0, 32};
}
std::string text;
if(!params->getQueryParameter("text", text)) {
error400(res, "Required parameter 'text' not set.");
return;
}
Bitmap bmp;
Font::textToBitmap(text.c_str(), &bmp, color);
WebServer::instance().m_animController->changeAnimation(
std::unique_ptr<Animation>(
new ImageScrollerAnimation(WebServer::instance().m_fader, &bmp, 5)),
false);
}
void WebServer::handleUpdate(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "text/plain");
httpsserver::ResourceParameters * params = req->getParams();
if(!params->isQueryParameterSet("content")) {
error400(res, "Send update image as 'content'.");
}
res->print("Content length: ");
res->println(req->getContentLength());
res->println("Content:");
res->println("--------------------------------------------------------------------------------");
uint8_t buf[128];
while(!req->requestComplete()) {
size_t bytes_processed = req->readBytes(buf, 128);
res->write(buf, bytes_processed);
}
res->println("--------------------------------------------------------------------------------");
std::vector<uint8_t> d;
d.push_back(1);
d.push_back(2);
d.push_back(3);
uint8_t sha256sum[32];
mbedtls_sha256_ret(d.data(), d.size(), sha256sum, 0);
for(size_t i = 0; i < 32; i++) {
static const char *conv = "0123456789abcdef";
uint8_t b = sha256sum[i];
res->print(conv[(b >> 4)]);
res->print(conv[(b &0x0F)]);
}
res->println();
}
void WebServer::handleChallenge(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "application/json");
std::ostringstream responseBuilder;
responseBuilder << "{ \"nonce\": " << WebServer::instance().m_cr.nonce() << "}";
res->println(responseBuilder.str().c_str());
}
void WebServer::handleAuthTest(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
res->setHeader("Content-Type", "text/plain");
httpsserver::ResourceParameters * params = req->getParams();
std::string response;
if(!params->getQueryParameter("response", response)) {
error400(res, "Parameter 'response' not set.");
return;
}
bool result = WebServer::instance().m_cr.verify(response);
if(result) {
res->println("Authentication test successful!");
} else {
res->println("Authentication test failed (invalid response)");
}
}
void WebServer::handleStatic(httpsserver::HTTPRequest *req, httpsserver::HTTPResponse *res)
{
std::string filename = "/htdocs" + req->getRequestString();
if(*(filename.end()-1) == '/') {
// looks like a directory name, so append 'index.html'
filename.append("index.html");
}
if(!serveFile(filename.c_str(), res)) {
req->discardRequestBody();
res->setStatusCode(404);
res->setHeader("Content-Type", "text/plain");
res->println("Error 404: Not found");
}
}
void WebServer::serverTask(void *arg)
{
httpsserver::ResourceNode *nodeRoot =
new httpsserver::ResourceNode("/", "GET", WebServer::handleRoot);
httpsserver::ResourceNode *nodeAPIColor =
new httpsserver::ResourceNode("/api/color", "GET", WebServer::handleColor);
httpsserver::ResourceNode *nodeAPISetAnim =
new httpsserver::ResourceNode("/api/setanim", "GET", WebServer::handleSetAnim);
httpsserver::ResourceNode *nodeAPIListAnim =
new httpsserver::ResourceNode("/api/listanim", "GET", WebServer::handleListAnim);
httpsserver::ResourceNode *nodeAPIText =
new httpsserver::ResourceNode("/api/text", "GET", WebServer::handleText);
httpsserver::ResourceNode *nodeAPIUpdate =
new httpsserver::ResourceNode("/api/update", "POST", WebServer::handleUpdate);
httpsserver::ResourceNode *nodeAPIChallenge =
new httpsserver::ResourceNode("/api/challenge", "GET", WebServer::handleChallenge);
httpsserver::ResourceNode *nodeAPIAuthTest =
new httpsserver::ResourceNode("/api/authtest", "GET", WebServer::handleAuthTest);
// handle all remaining requests by trying to serve static files. If no file is found, 404 is generated.
httpsserver::ResourceNode *nodeStatic =
new httpsserver::ResourceNode("", "GET", WebServer::handleStatic);
WebServer &server = WebServer::instance();
server.m_server->registerNode(nodeRoot);
server.m_server->registerNode(nodeAPIColor);
server.m_server->registerNode(nodeAPISetAnim);
server.m_server->registerNode(nodeAPIListAnim);
server.m_server->registerNode(nodeAPIText);
server.m_server->registerNode(nodeAPIUpdate);
server.m_server->registerNode(nodeAPIChallenge);
server.m_server->registerNode(nodeAPIAuthTest);
server.m_server->setDefaultNode(nodeStatic);
Serial.println("[server] Starting HTTP Server...");
server.m_server->start();
if (server.m_server->isRunning())
{
Serial.println("[server] Server ready.");
while (true)
{
server.m_server->loop();
delay(1);
}
}
Serial.println("[server] Server died?! This should never happen.");
}
bool WebServer::start(void)
{
xTaskCreatePinnedToCore(
WebServer::serverTask, /* Task function. */
"HTTP Server Task", /* name of task. */
6144, /* Stack size of task */
NULL, /* parameter of the task */
1, /* priority of the task */
NULL, /* Task handle to keep track of created task */
CORE_ID_WEBSERVER);
return true;
}