/* * This file is part of the "bluetoothheater" distribution * (https://gitlab.com/mrjones.id.au/bluetoothheater) * * Copyright (C) 2018 Ray Jones * Copyright (C) 2018 James Clark * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #define USE_EMBEDDED_WEBUPDATECODE #define HTTPS_LOGLEVEL 2 #include #include "BTCWifi.h" #include "BTCWebServer.h" #include "BTCota.h" #include "../Utility/DebugPort.h" #include "../Protocol/TxManage.h" #include "../Utility/helpers.h" #include "../cfg/pins.h" #include "../cfg/BTCConfig.h" #include "../Utility/BTC_JSON.h" #include "../Utility/Moderator.h" #include "../../lib/WiFiManager-dev/WiFiManager.h" #include #include "../Utility/NVStorage.h" #include #include #include #include "BrowserUpload.h" #include #include "WebContentDL.h" #include #include #include #include #include #include #include #include #include #include "../OLED/ScreenManager.h" #include "esp_task_wdt.h" // Max clients to be connected to the JSON handler #define MAX_CLIENTS 4 extern CScreenManager ScreenManager; using namespace httpsserver; extern WiFiManager wm; extern const char* stdHeader; extern const char* formatIndex; extern const char* updateIndex; extern const char* formatDoneContent; extern const char* rebootIndex; extern void checkSplashScreenUpdate(); size_t streamFileSSL(fs::File &file, const String& contentType, httpsserver::HTTPResponse* pSSL); void streamFileCoreSSL(const size_t fileSize, const String & fileName, const String & contentType); void processWebsocketQueue(); QueueHandle_t webSocketQueue = NULL; QueueHandle_t JSONcommandQueue = NULL; TaskHandle_t handleWebServerTask; #if USE_HTTPS == 1 SSLCert* pCert; HTTPSServer * secureServer; #endif HTTPServer * insecureServer; HTTPServer * WSserver; void SSLloopTask(void *); sBrowserUpload BrowserUpload; #ifdef OLD_SERVER // WebServer server(80); // WebSocketsServer webSocket = WebSocketsServer(81); #endif CGetWebContent GetWebContent; bool bRxWebData = false; // flags for OLED animation bool bTxWebData = false; bool bUpdateAccessed = false; // flag used to ensure browser update always starts via GET /update. direct accesses to POST /update will FAIL bool bFormatAccessed = false; bool bFormatPerformed = false; bool bStopWebServer = false; long _SuppliedFileSize = 0; bool checkFile(File &file); void addTableData(String& HTML, String dta); String getContentType(String filename); bool handleFileRead(String path, HTTPResponse* res=NULL); void onNotFound(); void onReboot(HTTPRequest * req, HTTPResponse * res); void onFormatSPIFFS(HTTPRequest * req, HTTPResponse * res); void onFormatNow(HTTPRequest * req, HTTPResponse * res); void onFormatDone(HTTPRequest * req, HTTPResponse * res); void rootRedirect(HTTPRequest * req, HTTPResponse * res); void onUpload(HTTPRequest* req, HTTPResponse* res); void onUploadBegin(HTTPRequest* req, HTTPResponse* res); void onUploadProgression(HTTPRequest* req, HTTPResponse* res); void onUploadCompletion(HTTPRequest* req, HTTPResponse* res); void onWMConfig(HTTPRequest* req, HTTPResponse* res); void onResetWifi(HTTPRequest* req, HTTPResponse* res); void doDefaultWebHandler(HTTPRequest * req, HTTPResponse * res); void build404Response(HTTPRequest * req, String& content, String file); void build500Response(String& content, String file); bool checkAuthentication(HTTPRequest * req, HTTPResponse * res, int credID=0); bool addRxJSONcommand(const char* str); bool checkRxJSONcommand(); // As websockets are more complex, they need a custom class that is derived from WebsocketHandler class JSONHandler : public WebsocketHandler { public: // This method is called by the webserver to instantiate a new handler for each // client that connects to the websocket endpoint static WebsocketHandler* create(); // This method is called when a message arrives void onMessage(WebsocketInputStreambuf * input); // Handler function on connection close void onClose(); }; // Simple array to store the active clients: JSONHandler* activeClients[MAX_CLIENTS]; // In the create function of the handler, we create a new Handler and keep track // of it using the activeClients array WebsocketHandler * JSONHandler::create() { DebugPort.println("Creating new JSON client!"); JSONHandler * handler = new JSONHandler(); for(int i = 0; i < MAX_CLIENTS; i++) { if (activeClients[i] == nullptr) { activeClients[i] = handler; break; } } return handler; } // When the websocket is closing, we remove the client from the array void JSONHandler::onClose() { for(int i = 0; i < MAX_CLIENTS; i++) { if (activeClients[i] == this) { activeClients[i] = nullptr; } } } void JSONHandler::onMessage(WebsocketInputStreambuf * inbuf) { // Get the input message std::ostringstream ss; std::string msg; ss << inbuf; msg = ss.str(); bRxWebData = true; // use a queue to hand over messages - ensures any commands that affect the I2C bus // (typ. various RTC operations) are performed in line with all other accesses addRxJSONcommand(msg.c_str()); } bool addRxJSONcommand(const char* str) { if(JSONcommandQueue) { char *pMsg = new char[strlen(str)+1]; strcpy(pMsg, str); xQueueSend(JSONcommandQueue, &pMsg, 0); return true; } return false; } bool checkRxJSONcommand() { char* pMsg = NULL; if(xQueueReceive(JSONcommandQueue, &pMsg, 0)) { interpretJsonCommand(pMsg); delete[] pMsg; return true; } return false; } // websocket handler, purely for performing /update handshaking class updateWSHandler : public WebsocketHandler { String renameFrom; String renameTo; public: // This method is called by the webserver to instantiate a new handler for each // client that connects to the websocket endpoint static WebsocketHandler* create(); // This method is called when a message arrives void onMessage(WebsocketInputStreambuf * input); // Handler function on connection close void onClose(); void interpret(const char* cmd); void decode(const char* cmd, String& payload); }; updateWSHandler* pUpdateHandler = NULL; WebsocketHandler* updateWSHandler::create() { if(pUpdateHandler) { delete pUpdateHandler; DebugPort.println("deleting old /update websocket!"); } DebugPort.println("Creating new /update websocket!"); pUpdateHandler = new updateWSHandler(); return pUpdateHandler; } void updateWSHandler::onMessage(WebsocketInputStreambuf * inbuf) { // DebugPort.printf("updateWSHandler::onMessage..."); // Get the input message std::ostringstream ss; std::string msg; ss << inbuf; msg = ss.str(); char cmd[256]; memset(cmd, 0, 256); if(msg.length() < 256) { strcpy(cmd, msg.c_str()); interpret(cmd); // send to the decode routine } } // When the websocket is closing, we remove the client from the array void updateWSHandler::onClose() { DebugPort.println("updateWSHandler::onClose()"); pUpdateHandler = nullptr; } void updateWSHandler::interpret(const char* cmd) { if(strlen(cmd) == 0) return; // DebugPort.printf("updateWSHandler::interpret %s...", cmd); StaticJsonBuffer<512> jsonBuffer; // create a JSON buffer on the heap JsonObject& obj = jsonBuffer.parseObject(cmd); if(!obj.success()) { // DebugPort.println(" FAILED"); return; } // DebugPort.println(" OK"); JsonObject::iterator it; for(it = obj.begin(); it != obj.end(); ++it) { String payload(it->value.as()); decode(it->key, payload); } } void updateWSHandler::decode(const char* cmd, String& payload) { if(strcmp("erase", cmd) == 0) { String filename(payload); filename.replace("%20", " "); // convert HTML spaces to real spaces if(filename.length() != 0) { DebugPort.printf("WS onErase: %s ", filename.c_str()); if(SPIFFS.exists(filename.c_str())) { SPIFFS.remove(filename.c_str()); DebugPort.println("ERASED\r\n"); } else DebugPort.println("NOT FOUND\r\n"); } if(pUpdateHandler) pUpdateHandler->send("{\"updateReload\":100}", WebsocketHandler::SEND_TYPE_TEXT); // activeClients[i]->send(pMsg, WebsocketHandler::SEND_TYPE_TEXT); } else if(strcmp("renameFrom", cmd) == 0) { renameFrom = payload; renameFrom.replace("%20", " "); // convert html spaces to real spaces } else if(strcmp("renameTo", cmd) == 0) { renameTo = payload; renameTo.replace("%20", " "); // convert html spaces to real spaces if(renameFrom != "" && renameTo != "") { DebugPort.printf("Renaming %s to %s\r\n", renameFrom.c_str(), renameTo.c_str()); SPIFFS.rename(renameFrom.c_str(), renameTo.c_str()); checkSplashScreenUpdate(); } if(pUpdateHandler) pUpdateHandler->send("{\"updateReload\":100}", WebsocketHandler::SEND_TYPE_TEXT); } else if(strcmp("updateSize", cmd) == 0) { setUploadSize(payload.toInt()); } } const char* getWebContent(bool start) { if(isWifiSTAConnected() && start) { GetWebContent.start(); } return GetWebContent.getFilename(); } #if USE_HTTPS == 1 SemaphoreHandle_t SSLSemaphore = NULL; void SSLkeyTask(void *) { DebugPort.println("SSL creation starting"); pCert = new SSLCert(); ABpreferences SSLkeyStore; SSLkeyStore.begin("SSLkeys"); if(SSLkeyStore.hasBytes("Certificate")) { ScreenManager.showBootMsg("Loading SSL cert."); DebugPort.println("Using stored SSL certificate"); int len; len = SSLkeyStore.getBytesLength("Certificate"); unsigned char* pCertData = new unsigned char[len]; // POTENTIAL LEAK HERE DUE TO LAME SSL LIBARY POINTER COPY SSLkeyStore.getBytes("Certificate", pCertData, len); pCert->setCert(pCertData, len); len = SSLkeyStore.getBytesLength("PrivateKey"); unsigned char* pPKData = new unsigned char[len]; // POTENTIAL LEAK HERE DUE TO LAME SSL LIBARY POINTER COPY SSLkeyStore.getBytes("PrivateKey", pPKData, len); pCert->setPK(pPKData, len); // vTaskDelay(10000); // TEST } else { DebugPort.println("Creating SSL certificate - this may take a while..."); ScreenManager.showBootMsg("Creating SSL cert."); int createCertResult = createSelfSignedCert( *pCert, KEYSIZE_2048, "CN=myesp.local,O=acme,C=US"); DebugPort.println("SSL certificate created"); if (createCertResult != 0) { DebugPort.printf("Error generating certificate"); } else { SSLkeyStore.putBytes("Certificate", pCert->getCertData(), pCert->getCertLength()); SSLkeyStore.putBytes("PrivateKey", pCert->getPKData(), pCert->getPKLength()); } } SSLkeyStore.end(); DebugPort.printf("Certificate: length = %d\r\n", pCert->getCertLength()); hexDump(pCert->getCertData(), pCert->getCertLength(), 32); DebugPort.println(""); DebugPort.printf("Private key: length = %d\r\n", pCert->getPKLength()); hexDump(pCert->getPKData(), pCert->getPKLength(), 32); DebugPort.println(""); xSemaphoreGive(SSLSemaphore); vTaskDelete(NULL); } #endif void initWebServer(void) { if (MDNS.begin("Afterburner")) { DebugPort.println("MDNS responder started"); } // ScreenManager.showBootMsg("Preparing SSL cert."); #if USE_HTTPS == 1 // create SSL certificate, but off load to a task with a BIG stack; SSLSemaphore = xSemaphoreCreateBinary(); xTaskCreate(SSLkeyTask, "SSLkeyTask", 16384, NULL, TASK_PRIORITY_SSL_CERT, // low priority as this blocks BIG time &handleWebServerTask); while(!xSemaphoreTake(SSLSemaphore, 250)) { ScreenManager.showBootWait(1); } ScreenManager.showBootWait(0); ScreenManager.showBootMsg("Starting web server"); vSemaphoreDelete(SSLSemaphore); secureServer = new HTTPSServer(pCert); #else ScreenManager.showBootMsg("Starting web server"); #endif WSserver = new HTTPServer(81); insecureServer = new HTTPServer(); DebugPort.println("HTTPS server created"); ResourceNode * WSnodeRoot = new ResourceNode("/", "GET", [](HTTPRequest * req, HTTPResponse * res){ res->print("Insecure websocket lives here!!!"); }); WebsocketNode * WebsktNode = new WebsocketNode("/", &JSONHandler::create); WebsocketNode * WebsktUpdateNode = new WebsocketNode("/update", &updateWSHandler::create); // setup limited websocket only capability on port 81 WSserver->registerNode(WSnodeRoot); WSserver->registerNode(WebsktNode); // setup websocket capability on port 80 & 443 insecureServer->registerNode(WebsktNode); #if USE_HTTPS == 1 secureServer->registerNode(WebsktNode); // associated secure websocket #endif ResourceNode * rebootNode = new ResourceNode("/reboot", "", &onReboot); ResourceNode * formatNode = new ResourceNode("/formatspiffs", "", &onFormatNow); ResourceNode * updateNode = new ResourceNode("/update", "", &onUpload); ResourceNode * wmconfigNode = new ResourceNode("/wmconfig", "GET", &onWMConfig); ResourceNode * resetwifiNode = new ResourceNode("/resetwifi", "GET", &onResetWifi); ResourceNode * defaultGet = new ResourceNode("/", "GET", &doDefaultWebHandler); insecureServer->registerNode(rebootNode); insecureServer->registerNode(formatNode); insecureServer->registerNode(updateNode); insecureServer->registerNode(WebsktUpdateNode); insecureServer->registerNode(resetwifiNode); insecureServer->registerNode(wmconfigNode); insecureServer->setDefaultNode(defaultGet); #if USE_HTTPS == 1 secureServer->registerNode(rebootNode); secureServer->registerNode(formatNode); secureServer->registerNode(updateNode); secureServer->registerNode(WebsktUpdateNode); secureServer->registerNode(resetwifiNode); secureServer->registerNode(wmconfigNode); secureServer->setDefaultNode(defaultGet); #endif WSserver->start(); insecureServer->start(); #if USE_HTTPS == 1 secureServer->start(); #endif DebugPort.println("HTTPS started"); JSONcommandQueue = xQueueCreate(50, sizeof(char*) ); // setup task to handle webserver webSocketQueue = xQueueCreate(50, sizeof(char*) ); bStopWebServer = false; xTaskCreate(SSLloopTask, "Web server task", 8192, // 16384, NULL, TASK_PRIORITY_SSL_LOOP, // low priority as this potentially blocks BIG time &handleWebServerTask); DebugPort.println("HTTP task started"); } void SSLloopTask(void *) { while(!bStopWebServer) { WSserver->loop(); insecureServer->loop(); #if USE_HTTPS == 1 secureServer->loop(); #endif processWebsocketQueue(); vTaskDelay(1); } WSserver->stop(); insecureServer->stop(); #if USE_HTTPS == 1 secureServer->stop(); #endif vTaskDelete(NULL); bStopWebServer = false; } // called by main sketch loop() bool doWebServer(void) { GetWebContent.manage(); BrowserUpload.queueProcess(); // manage data queued from web update checkRxJSONcommand(); return true; } void stopWebServer() { DebugPort.println("Requesting web server stop"); bStopWebServer = true; delay(100); } String getContentType(String filename) { // convert the file extension to the MIME type if (filename.endsWith(".html")) return "text/html"; else if (filename.endsWith(".css")) return "text/css"; else if (filename.endsWith(".js")) return "application/javascript"; else if (filename.endsWith(".ico")) return "image/x-icon"; else if (filename.endsWith(".bin")) return "application/octet-stream"; else if (filename.endsWith(".zip")) return "application/x-zip"; else if (filename.endsWith(".gz")) return "application/x-gzip"; return "text/plain"; } bool findFormArg(HTTPRequest * req, const char* Arg, std::string& value) { HTTPBodyParser *pParser = new HTTPMultipartBodyParser(req); while(pParser->nextField()) { std::string name = pParser->getFieldName(); DebugPort.printf("findArg: %s\r\n", name.c_str()); if (name == Arg) { DebugPort.println("found desired Arg"); char buf[512]; size_t readLength = pParser->read((byte *)buf, 512); value = std::string(buf, readLength); delete pParser; return true; } } delete pParser; return false; } void doDefaultWebHandler(HTTPRequest * req, HTTPResponse * res) { String path = req->getRequestString().c_str(); if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file if(path.indexOf("index.html") >= 0) { if(!checkAuthentication(req, res, 1)) { return; } } if (!handleFileRead(req->getRequestString().c_str(), res)) { // send it if it exists String message; build404Response(req, message, path); res->setStatusCode(404); res->setStatusText("Not found"); res->print(message.c_str()); } } bool checkAuthentication(HTTPRequest * req, HTTPResponse * res, int credID) { // Get login information from request // If you use HTTP Basic Auth, you can retrieve the values from the request. // The return values will be empty strings if the user did not provide any data, // or if the format of the Authorization header is invalid (eg. no Basic Method // for Authorization, or an invalid Base64 token) sCredentials creds = NVstore.getCredentials(); if(credID == 0 && strlen(creds.webUpdatePassword) == 0) return true; if(credID == 1 && strlen(creds.webPassword) == 0) return true; std::string reqUsername = req->getBasicAuthUser(); std::string reqPassword = req->getBasicAuthPassword(); // If the user entered login information, we will check it if (reqUsername.length() > 0 && reqPassword.length() > 0) { if (credID == 0 && reqUsername == creds.webUpdateUsername && reqPassword == creds.webUpdatePassword) { return true; } if (credID == 1 && reqUsername == creds.webUsername && reqPassword == creds.webPassword) { return true; } } res->setStatusCode(401); res->setStatusText("Unauthorized"); res->setHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); res->setHeader("Content-Type", "text/html"); res->println("401. Not authorized"); return false; } bool handleFileRead(String path, HTTPResponse *res) { // send the right file to the client (if it exists) DebugPort.println("handleFileRead original request: " + path); if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file path.replace("%20", " "); // convert HTML spaces to normal spaces String contentType = getContentType(path); // Get the MIME type String pathWithGz = path + ".gz"; DebugPort.println("handleFileRead conditioned request: " + path); if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { // If the file exists as a compressed archive, or normal if (SPIFFS.exists(pathWithGz)) { // If the compressed file exists path += ".gz"; DebugPort.println("handleFileRead now .gz request: " + path); } File file = SPIFFS.open(path, "r"); // Open it if(!checkFile(file)) { // check it is readable file.close(); // if not, close the file } if(!file) { DebugPort.println("\tFile exists, but could not be read?"); // dodgy file - throw error back to client String content; build500Response(content, path); if(res) { res->setStatusCode(500); res->setStatusText("Internal server error"); res->print(content.c_str()); } return false; // If the file is broken, return false } else { if(res) { streamFileSSL(file, contentType, res); } file.close(); // Then close the file return true; } } DebugPort.println("\tFile Not Found"); return false; // If the file doesn't exist, return false } size_t streamFileSSL(fs::File &file, const String& contentType, HTTPResponse* res) { String filename = file.name(); if (filename.endsWith("gz") && contentType != String("application/x-gzip") && contentType != String("application/octet-stream")) { res->setHeader("Content-Encoding", "gzip"); } res->setHeader("Content-Type", contentType.c_str()); res->setHeader("Content-Length", httpsserver::intToString(file.size())); DebugPort.print("Streaming"); // Read the file and write it to the response uint8_t buffer[256]; size_t progressdot = 0; size_t done = 0; size_t length = file.read(buffer, 256); while(length > 0) { size_t wr = res->write(buffer, length); if(wr > 0) { done += wr; if(done > progressdot) { DebugPort.print("."); progressdot += 1024; } } if(wr <= 0) break; length = file.read(buffer, 256); } return done; } const char* stdHeader = R"=====( )====="; const char* updateIndex = R"=====( Afterburner update

Afterburner update

)====="; const char* wmConfigIndex = R"=====( Launching Afterburner Wifi Manager

Launching Afterburner Wifi Manager

This page will automatically reload in 15 seconds, please wait.

If auto reload fails, try manually refreshing the web page

You may need to reconnect to the Afterburner's AP following the reboot

)====="; void onWMConfig(HTTPRequest * req, httpsserver::HTTPResponse * res) { DebugPort.println("WEB: GET /wmconfig"); res->print(wmConfigIndex); DebugPort.println("Starting web portal for wifi config"); wmReboot newMode(true); newMode.startPortal = true; newMode.eraseCreds = false; newMode.delay = 500; scheduleWMreboot(newMode); } void onResetWifi(HTTPRequest * req, httpsserver::HTTPResponse * res) { DebugPort.println("WEB: GET /resetwifi"); res->print("Start Config Portal - Resetting Wifi credentials!"); DebugPort.println("diconnecting client and wifi, then rebooting"); wmReboot newMode(true); newMode.startPortal = true; newMode.eraseCreds = true; newMode.delay = 500; scheduleWMreboot(newMode); } void rootRedirect(HTTPRequest * req, httpsserver::HTTPResponse * res) { res->setHeader("Location","/"); // reselect the update page res->setStatusCode(303); } // pass new data for websocket send via a queue bool sendWebSocketString(const char* Str) { if(webSocketQueue) { char* pMsg = new char[strlen(Str)+1]; strcpy(pMsg, Str); xQueueSend(webSocketQueue, &pMsg, 0); return true; } return false; } // query queue for new messages to send to websocket(s) void processWebsocketQueue() { char* pMsg; if(webSocketQueue) { if(xQueueReceive(webSocketQueue, &pMsg, 0)) { if(pMsg == NULL) { // DebugPort.println("websocket send NULL averted"); } else { // DebugPort.printf("websocket len=%d\r\n", strlen(pMsg)); for(int i=0; i< MAX_CLIENTS; i++) { if(activeClients[i]) { bTxWebData = true; // OLED tx data animation flag activeClients[i]->send(pMsg, WebsocketHandler::SEND_TYPE_TEXT); // DebugPort.println("->"); } } delete[] pMsg; } } } } bool isWebSocketClientChange() { static int prevNumClients = -1; #ifdef OLD_SERVER int numClients = webSocket.connectedClients(); #else int numClients = 0; for(int i=0; i< MAX_CLIENTS; i++) { if(activeClients[i]) numClients++; } #endif if(numClients != prevNumClients) { bool retval = numClients > prevNumClients; prevNumClients = numClients; if(retval) { DebugPort.println("Increased number of web socket clients, should reset JSON moderator"); return true; } } return false; } bool hasWebClientSpoken(bool reset) { bool retval = bRxWebData; if(reset) bRxWebData = false; return retval; } bool hasWebServerSpoken(bool reset) { bool retval = bTxWebData; if(reset) bTxWebData = false; return retval; } void setUploadSize(long val) { _SuppliedFileSize = val; }; // Sometimes SPIFFS gets corrupted (WTF?) // When this happens, you can see the files exist, but you cannot read them // This routine checks the file is readable. // Typical failure mechanism is read returns 0, and the WifiClient upload never progresses // The software watchdog then steps in after 15 seconds of that nonsense bool checkFile(File &file) { uint8_t buf[128]; bool bOK = true; size_t available = file.available(); while(available) { int toRead = (available > 128) ? 128 : available; int Read = file.read(buf, toRead); if(Read != toRead) { bOK = false; DebugPort.printf("SPIFFS precautionary file check failed for %s\r\n", file.name()); break; } available = file.available(); } file.seek(0); return bOK; } void listSPIFFS(const char * dirname, uint8_t levels, String& HTMLreport, int withHTMLanchors) { char msg[128]; File root = SPIFFS.open(dirname); if (!root) { sprintf(msg, "Failed to open directory \"%s\"", dirname); DebugPort.println(msg); HTMLreport += msg; HTMLreport += "
"; return; } if (!root.isDirectory()) { sprintf(msg, "\"%s\" is not a directory", dirname); DebugPort.println(msg); HTMLreport += msg; HTMLreport += "
"; return; } HTMLreport += "

Current SPIFFS contents:

"; // create HTML table header HTMLreport += R"=====( )====="; File file = root.openNextFile(); while (file) { HTMLreport += "\n"; if (file.isDirectory()) { addTableData(HTMLreport, "DIR"); addTableData(HTMLreport, file.name()); addTableData(HTMLreport, ""); addTableData(HTMLreport, ""); addTableData(HTMLreport, ""); sprintf(msg, " DIR : %s", file.name()); DebugPort.println(msg); if (levels) { listSPIFFS(file.name(), levels - 1, HTMLreport); } } else { String fn = file.name(); String ers; String rename; if(withHTMLanchors == 2) { String htmlNm = fn; htmlNm.replace(" ", "%20"); rename = ""; ers = ""; } if(withHTMLanchors) { String fn2; if(fn.endsWith(".html")) { // can hyperlink .html files fn2 = fn; } else if(fn.endsWith(".html.gz")) { // we can hyperlink .html.gz files but we must strip .gz extension for // the hyperlink otherwise you get asked if you want to download the .gz, not view web page! fn2 = fn; fn2.remove(fn2.length()-3, 3); // strip trailing ".gz" } if(fn2.length() != 0) { fn2.replace(" ", "%20"); // create hyperlink if web page file fn = "" + file.name() + ""; } } String sz( int(file.size())); addTableData(HTMLreport, ""); addTableData(HTMLreport, fn); addTableData(HTMLreport, sz); addTableData(HTMLreport, rename); addTableData(HTMLreport, ers); sprintf(msg, " FILE: %s SIZE: %d", fn.c_str(), file.size()); DebugPort.println(msg); } HTMLreport += "\n"; file = root.openNextFile(); } HTMLreport += "
Name Size
\n"; if(withHTMLanchors) { char usage[128]; int used = SPIFFS.usedBytes(); int total = SPIFFS.totalBytes(); float percent = used * 100. / total; sprintf(usage, "

Usage
%d / %d bytes (%.1f%%)\n

", used, total, percent); HTMLreport += usage; } } void addTableData(String& HTML, String dta) { HTML += ""; HTML += dta; HTML += "\n"; } // function called upon completion of file (form) upload void onUploadCompletion(HTTPRequest * req, HTTPResponse * res) { _SuppliedFileSize = 0; DebugPort.println("WEB: POST /updatenow completion"); // completion functionality if(BrowserUpload.isSPIFFSupload()) { if(BrowserUpload.isOK()) { checkSplashScreenUpdate(); DebugPort.println("WEB: SPIFFS OK"); // server.send(200, "text/plain", "OK - File uploaded to SPIFFS"); res->setStatusCode(200); res->setHeader("Content-Type", "text/plain"); res->print("OK - File uploaded to SPIFFS"); // javascript reselects the /update page! } else { DebugPort.println("WEB: SPIFFS FAIL"); // server.send(500, "text/plain", "500: couldn't create file"); res->setStatusCode(500); res->setHeader("Content-Type", "text/plain"); res->print("500: couldn't create file"); } BrowserUpload.reset(); #if USE_SSL_LOOP_TASK != 1 ShowOTAScreen(-1, eOTAbrowser); // browser update #endif if(pUpdateHandler) pUpdateHandler->send("{\"updateReload\":1000}", WebsocketHandler::SEND_TYPE_TEXT); } else { if(BrowserUpload.isOK()) { DebugPort.println("WEB: FIRMWARE UPDATE OK"); // server.send(200, "text/plain", "OK - Afterburner will reboot shortly"); res->setStatusCode(200); res->setHeader("Content-Type", "text/plain"); res->print("OK - Afterburner will reboot shortly"); } else { DebugPort.println("WEB: FIRMWARE UPDATE FAIL"); // server.send(200, "text/plain", "FAIL - Afterburner will reboot shortly"); res->setStatusCode(200); res->setHeader("Content-Type", "text/plain"); res->print("FAIL - Afterburner will reboot shortly"); } // rootRedirect(req, res); forceBootInit(); // initate reboot const char* content[2]; content[0] = "New firmware upload"; content[1] = "completed"; ScreenManager.showRebootMsg(content, 1000); } } void onUpload(HTTPRequest* req, HTTPResponse* res) { if(req->getMethod() == "GET") { onUploadBegin(req, res); } if(req->getMethod() == "POST") { onUploadProgression(req, res); } } void onUploadBegin(HTTPRequest* req, HTTPResponse* res) { DebugPort.println("WEB: GET /update"); if(!checkAuthentication(req, res)) return; bUpdateAccessed = true; bFormatAccessed = false; bFormatPerformed = false; #ifdef USE_EMBEDDED_WEBUPDATECODE String SPIFFSinfo; listSPIFFS("/", 2, SPIFFSinfo, 2); String content = stdHeader; content += updateIndex; // content += "

" + SPIFFSinfo + "
"; content += SPIFFSinfo; content += "

"; content += ""; res->setStatusCode(200); res->setHeader("Content-Type", "text/html"); res->print( content ); res->finalize(); #else handleFileRead("/uploadfirmware.html"); #endif } void onUploadProgression(HTTPRequest * req, httpsserver::HTTPResponse * res) { char JSON[64]; if(!bUpdateAccessed) { // only allow progression via /update, attempts to directly access /updatenow will fail DebugPort.println("WEB: POST /update forbidden entry"); res->setHeader("Location","/update"); // reselect the update page res->setStatusCode(303); } else { if(!checkAuthentication(req, res)) { // attempt to POST without using /update - forced redirect to root bUpdateAccessed = false; return; } HTTPUpload upload; HTTPBodyParser *parser; std::string contentType = req->getHeader("Content-Type"); size_t semicolonPos = contentType.find(";"); if (semicolonPos != std::string::npos) { contentType = contentType.substr(0, semicolonPos); } if (contentType == "multipart/form-data") { parser = new HTTPMultipartBodyParser(req); } else { Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); return; } // We iterate over the fields. Any field with a filename is uploaded while(parser->nextField()) { std::string name = parser->getFieldName(); std::string sfilename = parser->getFieldFilename(); std::string mimeType = parser->getFieldMimeType(); DebugPort.printf("onUploadProgression: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), sfilename.c_str(), mimeType.c_str()); // Double check that it is what we expect if (name != "update") { Serial.println("Skipping unexpected field"); break; } // Should check file name validity and all that, but we skip that. String filename = sfilename.c_str(); if(filename[0] != '/') filename = "/" + filename; upload.filename = filename; upload.name = name.c_str(); upload.type = mimeType.c_str(); upload.totalSize = 0; upload.currentSize = 0; int sts = BrowserUpload.begin(filename, _SuppliedFileSize); // _SuppliedFileSize come in via websocket if(sts < 0) { break; } else { if(pUpdateHandler) { sprintf(JSON, "{\"updateProgress\":%d}", sts); pUpdateHandler->send(JSON, WebsocketHandler::SEND_TYPE_TEXT); } } while (!parser->endOfField()) { // file upload and writing to SPIFFS is not a happy combination as the web server is running at an elevated level here // best to pass the data to the normal Arduino processing task via a queue, but maintain synchronism with the processing // by spinning here until ready. while(!BrowserUpload.Ready()) { taskYIELD(); } esp_task_wdt_reset(); upload.currentSize = parser->read(upload.buf, HTTP_UPLOAD_BUFLEN); BrowserUpload.queueFragment(upload); // let user task process the fresh data while(!BrowserUpload.Ready()) { taskYIELD(); } esp_task_wdt_reset(); // sts = BrowserUpload.fragment(upload, res); sts = BrowserUpload.queueResult(); if(sts < 0) { if(pUpdateHandler) { sprintf(JSON, "{\"updateProgress\":%d}", sts); pUpdateHandler->send(JSON, WebsocketHandler::SEND_TYPE_TEXT); } DebugPort.printf("Upload code %d\r\n", sts); break; } else { // upload still in progress? if(BrowserUpload.bUploadActive) { // show progress unless a write error has occured // DebugPort.printf(" p%d ", uxTaskPriorityGet(NULL)); // DebugPort.print("."); if(upload.totalSize) { // feed back bytes received over web socket for progressbar update on browser (via javascript) if(pUpdateHandler) { sprintf(JSON, "{\"updateProgress\":%d}", upload.totalSize); pUpdateHandler->send(JSON, WebsocketHandler::SEND_TYPE_TEXT); } } /* // show percentage on OLED int percent = 0; if(_SuppliedFileSize) percent = 100 * upload.totalSize / _SuppliedFileSize; #if USE_SSL_LOOP_TASK != 1 ShowOTAScreen(percent, eOTAbrowser); // browser update #endif */ } } } sts = BrowserUpload.end(upload); if(pUpdateHandler) { sprintf(JSON, "{\"updateProgress\":%d}", sts); pUpdateHandler->send(JSON, WebsocketHandler::SEND_TYPE_TEXT); if(!BrowserUpload.isSPIFFSupload()) { sprintf(JSON, "{\"updateDone\":1}"); pUpdateHandler->send(JSON, WebsocketHandler::SEND_TYPE_TEXT); } } } bUpdateAccessed = false; // close gate on POST to /updatenow delete parser; onUploadCompletion(req, res); res->finalize(); } } /*************************************************************************************** * FORMAT SPIFFS HANDLING * * User must first access /formatspiffs. * If not already authenticated, an Username/Password challenge is presented * If that passes, bFormatAccessed is set, unlocking access to the /formatnow path * The presneted web page offers Format and Cancel button. * Cancel will immediatly return to the file upload path '/update' * Format will then present a confirmation dialog, user must press Yes to proceed. * * Assuming Yes was pressed, a HTTP POST to /format now with the payload 'confirm'='yes' is performed * The /formatnow handler will check that confirm does equal yes, and that bFormatAccessed was set * If all good SPIFFS is re-formatted - no response is set. * The javascript though from the /formatspiffs page performs a reload shortly after the post (200ms timeout) * * As bFormatAccessed is still set, a confimration page is the presented advising files now need to be uploaded * A button allows direct access to /update */ void onFormatSPIFFS(HTTPRequest * req, HTTPResponse * res) { DebugPort.println("WEB: GET /formatspiffs"); bUpdateAccessed = false; String content = stdHeader; if(!bFormatPerformed) { if(checkAuthentication(req, res)) { bFormatAccessed = true; // only set after we pass authentication content += formatIndex; res->setStatusCode(200); res->setHeader("Content-Type", "text/html"); res->print( content ); } } else { bFormatAccessed = false; bFormatPerformed = false; content += formatDoneContent; res->setStatusCode(200); res->setHeader("Content-Type", "text/html"); res->print( content ); } res->finalize(); } const char* formatDoneContent = R"=====(

SPIFFS partition has been formatted

You must now upload the web content.

Latest web content can be downloaded from http://afterburner.mrjones.id.au/firmware.html

Please ensure you unzip the web page content, then upload all the files contained.

)====="; const char* formatIndex = R"=====( Afterburner SPIFFS format

Format SPIFFS partition

CAUTION! This will erase all web content


)====="; void onFormatNow(HTTPRequest * req, httpsserver::HTTPResponse * res) { if(req->getMethod() == "GET") { onFormatSPIFFS(req, res); // DebugPort.println("WEB: GET /formatnow - ILLEGAL - root redirect"); // rootRedirect(req, res); } if(req->getMethod() == "POST") { // HTTP POST handler, do not need to return a web page! DebugPort.println("WEB: POST /formatnow"); std::string value; findFormArg(req, "confirm", value); if(value == "yes" && bFormatAccessed) { // confirm user agrees, and we did pass thru /formatspiffs first DebugPort.println("Formatting SPIFFS partition"); SPIFFS.format(); // re-format the SPIFFS partition bFormatPerformed = true; } else { bFormatAccessed = false; // user cancelled upon last confirm popup, or not authenticated access bFormatPerformed = false; rootRedirect(req, res); } } } void onReboot(HTTPRequest * req, httpsserver::HTTPResponse * res) { if (req->getMethod() == "GET") { DebugPort.println("WEB: GET /reboot"); String content = stdHeader; content += rebootIndex; res->print(content); res->finalize(); } if (req->getMethod() == "POST") { // HTTP POST handler, do not need to return a web page! DebugPort.println("WEB: POST /reboot"); // First, we need to check the encoding of the form that we have received. // The browser will set the Content-Type request header, so we can use it for that purpose. std::string value; if(findFormArg(req, "reboot", value)) { if(value == "yes") { // confirm user agrees, and we did pass thru /formatspiffs first DebugPort.println("Rebooting via /reboot"); // initate reboot const char* content[2]; content[0] = "/reboot"; content[1] = "initiated"; ScreenManager.showRebootMsg(content, 1000); // ESP.restart(); } } } } const char* rebootIndex = R"=====( Afterburner Reboot

Reboot Afterburner

     )====="; /*************************************************************************************** * HTTP RESPONSE 404 - FILE NOT FOUND HANDLING */ void build404Response(HTTPRequest * req, String& content, String file) { content += stdHeader; content += R"=====(

404: File Not Found

URI: )====="; content += file; content += R"=====(
Method: )====="; content += req->getMethod().c_str(); content += "
Arguments: "; for(auto it = req->getParams()->beginQueryParameters(); it != req->getParams()->endQueryParameters(); ++it) { std::string nm(it->first); std::string val(it->second); content += " "; content += nm.c_str(); content += ": "; content += val.c_str(); content += "
"; } content += R"=====(


Please check the URL.
If OK please try uploading the file from the web content.

Latest web content can be downloaded from http://afterburner.mrjones.id.au/firmware.html

Please ensure you unzip the web page content, then upload all the files contained.


)====="; String SPIFFSinfo; listSPIFFS("/", 2, SPIFFSinfo, 1); content += SPIFFSinfo; content += ""; content += ""; } /*************************************************************************************** * HTTP RESPONSE 500 - SERVER ERROR HANDLING */ void build500Response(String& content, String file) { content = stdHeader; content += R"=====(

500: Internal Server Error

Sorry, cannot open file

")====="; content += file; content += R"=====(" exists, but cannot be streamed?


Recommended remedy is to re-format the SPIFFS partition, then reload the web content files.
Latest web content can be downloaded from http://afterburner.mrjones.id.au/firmware.html (opens in new page).

To format the SPIFFS partition, press

You will then need to upload each file of the web content by using the subsequent "Upload" button.


Please ensure you unzip the web page content, then upload all the files contained.

)====="; }