/* HTTP Server setup and handler functions */ #include #include #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(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( 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(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( 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 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; }