/* * 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 #include "BTCWebServer.h" #include "../Utility/DebugPort.h" #include "../Protocol/TxManage.h" #include "../Utility/helpers.h" #include "../cfg/pins.h" #include "../cfg/BTCConfig.h" #include "Index.h" #include "../Utility/BTC_JSON.h" #include "../Utility/Moderator.h" #include #include #include #include "../Utility/NVStorage.h" extern WiFiManager wm; File fsUploadFile; // a File object to temporarily store the received file int SPIFFSupload = 0; WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); bool bRxWebData = false; // flags for OLED animation bool bTxWebData = false; bool bUpdateAccessed = false; // flag used to ensure web update always starts via /update. direct accesses to /updatenow will FAIL long _SuppliedFileSize = 0; void handleBTCNotFound(); bool checkFile(File &file); 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"; return "text/plain"; } bool handleFileRead(String path) { // send the right file to the client (if it exists) DebugPort.println("handleFileRead: " + path); if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file String contentType = getContentType(path); // Get the MIME type if (SPIFFS.exists(path)) { // If the file exists File file = SPIFFS.open(path, "r"); // Open it if(!checkFile(file)) { // check it is readable file.close(); // Then close the file again } if(!file) { DebugPort.println("\tFile exists, but could not be read?"); String SPIFFSfmtpath = "http://" + server.client().localIP().toString() + "/formatspiffs"; String Updatepath = "http://" + server.client().localIP().toString() + "/update"; String message = "

Internal Server Error

"; message += "

Sorry, cannot open file

"; message += "

" + path + " exists, but cannot be opened?
"; message += "Recommended remedy is to re-format SPIFFS, then reload the web content."; message += "

Use:
" + SPIFFSfmtpath + "
to format SPIFFS."; message += "

Then:
" + Updatepath + "
to upload each file of the web content.
"; message += "

Latest web content can be downloaded from http://www.mrjones.id.au/afterburner/firmware.html"; message += "

Please ensure you unzip the web page content, then upload all the contained files."; server.send(500, "text/html", message); } else { server.streamFile(file, contentType); // And send it to the client file.close(); // Then close the file again } return true; } DebugPort.println("\tFile Not Found"); return false; // If the file doesn't exist, return false } void handleWMConfig() { DebugPort.println("WEB: GET /wmconfig"); server.send(200, "text/plain", "Start Config Portal - Retaining credential"); DebugPort.println("Starting web portal for wifi config"); delay(500); wifiEnterConfigPortal(true, false, 3000); } void handleReset() { DebugPort.println("WEB: GET /resetwifi"); server.send(200, "text/plain", "Start Config Portal - Resetting Wifi credentials!"); DebugPort.println("diconnecting client and wifi, then rebooting"); delay(500); wifiEnterConfigPortal(true, true, 3000); } void handleFormat() { DebugPort.println("WEB: GET /formatspiffs"); String Updatepath = "http://" + server.client().localIP().toString() + "/update"; String message = "

SPIFFS partition formatted

"; message += "

You must now upload the web content.

"; message += "

Latest web content can be downloaded from http://www.mrjones.id.au/afterburner/firmware.html"; message += "

Use:
" + Updatepath + "
to then upload each file of the web content.
"; message += "

Please ensure you unzip the web page content, then upload all the contained files."; server.send(200, "text/html", message); DebugPort.println("Formatting SPIFFS partition"); delay(500); SPIFFS.format(); } void handleSpiffs() { String report; String message; listDir(SPIFFS, "/", 2, report, true); char usage[128]; sprintf(usage, "

Usage: %d/%d

", SPIFFS.usedBytes(), SPIFFS.totalBytes()); message += "

Current SPIFFS contents:

"; message += report; message += usage; message += "

Add more files
"; message += "

Home"; server.send(200, "text/html", message); } void handleBTCNotFound() { String path = server.uri(); if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file String Updatepath = "http://" + server.client().localIP().toString() + "/update"; String message = "

404: File Not Found

"; message += "

URI: " + path + ""; message += "
Method: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "
Arguments: "; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "
"; } message += "


"; message += "

Please try uploading the file from the web content."; message += "

Latest web content can be downloaded from http://www.mrjones.id.au/afterburner/firmware.html"; message += "

Use:
" + Updatepath + "
to upload the web content.
"; message += "

Please ensure you unzip the web page content, then upload all the contained files."; char usage[128]; sprintf(usage, "

Usage: %d/%d

", SPIFFS.usedBytes(), SPIFFS.totalBytes()); String report; listDir(SPIFFS, "/", 2, report); message += "


Current SPIFFS contents:

"; message += report; message += usage; server.send(404, "text/html", message); } // embedded HTML & Javascript to perform browser based updates of firmware or SPIFFS const char* updateIndex = R"=====( Afterburner firmware update

Afterburner firmware update




)====="; void rootRedirect() { server.sendHeader("Location","/"); // reselect the update page server.send(303); } void initWebServer(void) { Update .onProgress([](unsigned int progress, unsigned int total) { int percent = (progress / (total / 100)); DebugPort.printf("Progress: %u%%\r", percent); DebugPort.handle(); // keep telnet spy alive ShowOTAScreen(percent, eOTAWWW); // WWW update in place DebugPort.print("^"); }); if (MDNS.begin("Afterburner")) { DebugPort.println("MDNS responder started"); } server.on("/wmconfig", handleWMConfig); server.on("/resetwifi", handleReset); server.on("/formatspiffs", handleFormat); server.on("/spiffs", handleSpiffs); server.on("/tst", HTTP_GET, []() { DebugPort.println("WEB: GET /tst"); server.sendHeader("Location","/"); // reselect the update page server.send(303); }); // Magical code originally shamelessly lifted from Arduino WebUpdate example, then modified // This allows pushing new firmware to the ESP from a WEB BROWSER! // Added authentication and a sequencing flag to ensure this is not bypassed // // Initial launch page server.on("/update", HTTP_GET, []() { DebugPort.println("WEB: GET /update"); sCredentials creds = NVstore.getCredentials(); if (!server.authenticate(creds.webUpdateUsername, creds.webUpdatePassword)) { return server.requestAuthentication(); } bUpdateAccessed = true; #ifdef USE_EMBEDDED_WEBUPDATECODE server.send(200, "text/html", updateIndex); #else handleFileRead("/uploadfirmware.html"); #endif }); // handle attempts to just browse the /updatenow path - force redirect to root server.on("/updatenow", HTTP_GET, []() { DebugPort.println("WEB: GET /updatenow - ILLEGAL - root redirect"); rootRedirect(); }); // actual guts that manages the new firmware upload server.on("/updatenow", HTTP_POST, []() { DebugPort.println("WEB: POST /updatenow completion"); // completion functionality if(SPIFFSupload) { if(SPIFFSupload == 1) { DebugPort.println("WEB: SPIFFS OK"); server.send(200, "text/plain", "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"); } SPIFFSupload = 0; } else { if(Update.hasError()) { DebugPort.println("WEB: UPDATE FAIL"); server.send(200, "text/plain", "FAIL - Afterburner will reboot shortly"); } else { DebugPort.println("WEB: UPDATE OK"); server.send(200, "text/plain", "OK - Afterburner will reboot shortly"); } delay(1000); // javascript redirects to root page so we go there after reboot! forceBootInit(); ESP.restart(); // reboot } }, []() { if(bUpdateAccessed) { // only allow progression via /update, attempts to directly access /updatenow will fail HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { String filename = upload.filename; DebugPort.setDebugOutput(true); if(filename.endsWith(".bin")) { DebugPort.printf("Update: %s %d\r\n", filename.c_str(), upload.totalSize); if (!Update.begin()) { //start with max available size Update.printError(DebugPort); } } else { if(!filename.startsWith("/")) filename = "/"+filename; DebugPort.printf("handleFileUpload Name: %s\r\n", filename.c_str()); fsUploadFile = SPIFFS.open(filename, "w"); // Open the file for writing in SPIFFS (create if it doesn't exist) SPIFFSupload = fsUploadFile ? 1 : 2; //filename = String(); } } // handle file segments else if (upload.status == UPLOAD_FILE_WRITE) { #if USE_SW_WATCHDOG == 1 feedWatchdog(); // we get stuck here for a while, don't let the watchdog bite! #endif if(upload.totalSize) { char JSON[64]; sprintf(JSON, "{\"progress\":%d}", upload.totalSize); sendWebServerString(JSON); // feedback proper byte count of update to browser via websocket } int percent = 0; if(_SuppliedFileSize) percent = 100 * upload.totalSize / _SuppliedFileSize; ShowOTAScreen(percent, eOTAbrowser); // browser update DebugPort.print("."); if(fsUploadFile) { fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file } else { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(DebugPort); } } } // handle end of upload else if (upload.status == UPLOAD_FILE_END) { if(SPIFFSupload) { if(fsUploadFile) { fsUploadFile.close(); // Close the file again DebugPort.printf("handleFileUpload Size: %d\r\n", upload.totalSize); } } else { if (Update.end(true)) { //true to set the size to the current progress DebugPort.printf("Update Success: %u\r\nRebooting...\r\n", upload.totalSize); } else { Update.printError(DebugPort); } } DebugPort.setDebugOutput(false); bUpdateAccessed = false; } else { DebugPort.printf("Update Failed Unexpectedly (likely broken connection): status=%d\r\n", upload.status); bUpdateAccessed = false; } } else { // attempt to POST without using /update - forced redirect to root DebugPort.println("WEB: POST /updatenow forbidden entry"); rootRedirect(); } }); // NOTE: this serves the default home page, and favicon.ico server.onNotFound([]() { // If the client requests any URI if (!handleFileRead(server.uri())) { // send it if it exists handleBTCNotFound(); } }); server.begin(); webSocket.begin(); webSocket.onEvent(webSocketEvent); DebugPort.println("HTTP server started"); } // called by main sketch loop() bool doWebServer(void) { webSocket.loop(); server.handleClient(); return true; } bool isWebServerClientChange() { static int prevNumClients = -1; int numClients = webSocket.connectedClients(); if(numClients != prevNumClients) { prevNumClients = numClients; DebugPort.println("Changed number of web clients, should reset JSON moderator"); return true; } return false; } bool sendWebServerString(const char* Str) { CProfile profile; if(webSocket.connectedClients()) { unsigned long tCon = profile.elapsed(true); bTxWebData = true; // OLED tx data animation flag webSocket.broadcastTXT(Str); unsigned long tWeb = profile.elapsed(true); DebugPort.printf("Websend times : %ld,%ld\r\n", tCon, tWeb); return true; } return false; } void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { if (type == WStype_TEXT) { bRxWebData = true; char cmd[256]; memset(cmd, 0, 256); for (int i = 0; i < length && i < 256; i++) { cmd[i] = payload[i]; } interpretJsonCommand(cmd); // send to the main heater controller decode routine } } 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 listDir(fs::FS &fs, const char * dirname, uint8_t levels, String& HTMLreport, bool withHTMLanchors) { char msg[128]; File root = fs.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; } File file = root.openNextFile(); while (file) { if (file.isDirectory()) { sprintf(msg, " DIR : %s", file.name()); DebugPort.println(msg); HTMLreport += msg; HTMLreport += "
"; if (levels) { listDir(fs, file.name(), levels - 1, HTMLreport); } } else { String fn = file.name(); if(withHTMLanchors) { if(fn.endsWith(".html") || fn.endsWith(".htm")) { String fn2(fn); fn = "" + fn2 + ""; } } sprintf(msg, " FILE: %s SIZE: %d", fn.c_str(), file.size()); DebugPort.println(msg); HTMLreport += msg; HTMLreport += "
"; } file = root.openNextFile(); } }