#include "esp-fs-webserver.h" FSWebServer::FSWebServer(fs::FS &fs, WebServerClass &server) { m_filesystem = &fs; webserver = &server; m_basePath[0] = '\0'; } WebServerClass *FSWebServer::getRequest() { return webserver; } void FSWebServer::run() { webserver->handleClient(); if (m_apmode) m_dnsServer.processNextRequest(); } void FSWebServer::addHandler(const Uri &uri, HTTPMethod method, WebServerClass::THandlerFunction fn) { webserver->on(uri, method, fn); } void FSWebServer::addHandler(const Uri &uri, WebServerClass::THandlerFunction handler) { webserver->on(uri, HTTP_ANY, handler); } // List all files saved in the selected filesystem bool FSWebServer::checkDir(char *dirname, uint8_t levels) { if (dirname[0] != '/') dirname[0] = '/'; File root = m_filesystem->open(dirname, "r"); if (!root) { DebugPrintln("- failed to open directory\n"); return false; } if (!root.isDirectory()) { DebugPrintln(" - not a directory\n"); return false; } File file = root.openNextFile(); while (file) { if (file.isDirectory()) { char dir[16]; strcpy(dir, "/"); strcat(dir, file.name()); DebugPrintf("DIR : %s\n", dir); checkDir(dir, levels - 1); } else { DebugPrintf(" FILE: %s\tSIZE: %d\n", file.name(), file.size()); } file = root.openNextFile(); } return true; } bool FSWebServer::begin(const char *path) { DebugPrintln("\nList the files of webserver: "); if (path != nullptr) strcpy(m_basePath, path); m_fsOK = checkDir(m_basePath, 2); #ifdef INCLUDE_EDIT_HTM webserver->on("/status", HTTP_GET, std::bind(&FSWebServer::handleStatus, this)); webserver->on("/list", HTTP_GET, std::bind(&FSWebServer::handleFileList, this)); webserver->on("/edit", HTTP_GET, std::bind(&FSWebServer::handleGetEdit, this)); webserver->on("/edit", HTTP_PUT, std::bind(&FSWebServer::handleFileCreate, this)); webserver->on("/edit", HTTP_DELETE, std::bind(&FSWebServer::handleFileDelete, this)); #endif webserver->onNotFound(std::bind(&FSWebServer::handleRequest, this)); webserver->on("/favicon.ico", HTTP_GET, std::bind(&FSWebServer::replyOK, this)); webserver->on("/", HTTP_GET, std::bind(&FSWebServer::handleIndex, this)); #ifdef INCLUDE_SETUP_HTM webserver->on("/setup", HTTP_GET, std::bind(&FSWebServer::handleSetup, this)); #endif webserver->on("/scan", HTTP_GET, std::bind(&FSWebServer::handleScanNetworks, this)); webserver->on("/connect", HTTP_POST, std::bind(&FSWebServer::doWifiConnection, this)); webserver->on("/restart", HTTP_GET, std::bind(&FSWebServer::doRestart, this)); webserver->on("/ipaddress", HTTP_GET, std::bind(&FSWebServer::getIpAddress, this)); // Captive Portal redirect webserver->on("/redirect", HTTP_GET, std::bind(&FSWebServer::captivePortal, this)); // Windows webserver->on("/connecttest.txt", HTTP_GET, std::bind(&FSWebServer::captivePortal, this)); // Apple webserver->on("/hotspot-detect.html", HTTP_GET, std::bind(&FSWebServer::captivePortal, this)); // Android webserver->on("/generate_204", HTTP_GET, std::bind(&FSWebServer::captivePortal, this)); webserver->on("/gen_204", HTTP_GET, std::bind(&FSWebServer::captivePortal, this)); // Upload file // - first callback is called after the request has ended with all parsed arguments // - second callback handles file upload at that location webserver->on("/edit", HTTP_POST, std::bind(&FSWebServer::replyOK, this), std::bind(&FSWebServer::handleFileUpload, this)); // OTA update via webbrowser m_httpUpdater.setup(webserver); #ifdef ESP32 webserver->enableCrossOrigin(true); #endif webserver->setContentLength(50); webserver->begin(); return true; } void FSWebServer::setCaptiveWebage(const char *url) { m_apWebpage = (char *)realloc(m_apWebpage, sizeof(url)); strcpy(m_apWebpage, url); } IPAddress FSWebServer::setAPmode(const char *ssid, const char *psk) { m_apmode = true; WiFi.mode(WIFI_AP_STA); WiFi.persistent(false); WiFi.softAP(ssid, psk); /* Setup the DNS server redirecting all the domains to the apIP */ m_dnsServer.setErrorReplyCode(DNSReplyCode::NoError); m_dnsServer.start(53, "*", WiFi.softAPIP()); return WiFi.softAPIP(); } IPAddress FSWebServer::startWiFi(uint32_t timeout, const char *apSSID, const char *apPsw) { IPAddress ip; m_timeout = timeout; WiFi.mode(WIFI_STA); const char *_ssid; const char *_pass; #if defined(ESP8266) struct station_config conf; wifi_station_get_config_default(&conf); _ssid = reinterpret_cast(conf.ssid); _pass = reinterpret_cast(conf.password); #elif defined(ESP32) wifi_config_t conf; esp_wifi_get_config(WIFI_IF_STA, &conf); _ssid = reinterpret_cast(conf.sta.ssid); _pass = reinterpret_cast(conf.sta.password); #endif if (strlen(_ssid) && strlen(_pass)) { WiFi.begin(_ssid, _pass); Serial.print(F("Connecting to ")); Serial.println(_ssid); uint32_t startTime = millis(); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (WiFi.status() == WL_CONNECTED) { ip = WiFi.localIP(); return ip; } // If no connection after a while go in Access Point mode if (millis() - startTime > m_timeout) break; } } if (apSSID != nullptr && apPsw != nullptr) setAPmode(apSSID, apPsw); else setAPmode("ESP_AP", "123456789"); WiFi.begin(); ip = WiFi.softAPIP(); Serial.print(F("\nAP mode.\nServer IP address: ")); Serial.println(ip); Serial.println(); return ip; } //////////////////////////////// WiFi ///////////////////////////////////////// /** * Redirect to captive portal if we got a request for another domain. */ bool FSWebServer::captivePortal() { IPAddress ip = webserver->client().localIP(); char serverLoc[sizeof("https:://255.255.255.255/") + sizeof(m_apWebpage) + 1]; snprintf(serverLoc, sizeof(serverLoc), "http://%d.%d.%d.%d%s", ip[0], ip[1], ip[2], ip[3], m_apWebpage); // redirect if hostheader not server ip, prevent redirect loops if (strcmp(serverLoc, webserver->hostHeader().c_str())) { webserver->sendHeader(F("Location"), serverLoc, true); webserver->send(302, F("text/html"), ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. webserver->client().stop(); // Stop is needed because we sent no content length return true; } return false; } void FSWebServer::handleRequest() { if (!m_fsOK) { replyToCLient(ERROR, PSTR(FS_INIT_ERROR)); return; } #if defined(ESP32) String _url = WebServer::urlDecode(webserver->uri()); #elif defined(ESP8266) String _url = ESP8266WebServer::urlDecode(webserver->uri()); #endif // First try to find and return the requested file from the filesystem, // and if it fails, return a 404 page with debug information // Serial.print("urlDecode: "); // Serial.println(_url); if (handleFileRead(_url)) return; else replyToCLient(NOT_FOUND, PSTR(FILE_NOT_FOUND)); } void FSWebServer::getIpAddress() { webserver->send(200, "text/json", WiFi.localIP().toString()); } void FSWebServer::doRestart() { Serial.println("RESTART"); webserver->send(200, "text/json", "Going to restart ESP"); delay(500); ESP.restart(); } void FSWebServer::doWifiConnection() { String ssid, pass; bool persistent = true; WiFi.mode(WIFI_AP_STA); if (webserver->hasArg("ssid")) { ssid = webserver->arg("ssid"); } if (webserver->hasArg("password")) { pass = webserver->arg("password"); } if (webserver->hasArg("persistent")) { String pers = webserver->arg("persistent"); if (pers.equals("false")) { persistent = false; } } if (WiFi.status() == WL_CONNECTED) { IPAddress ip = WiFi.localIP(); String resp = "ESP is currently connected to a WiFi network.

" "Actual connection will be closed and a new attempt will be done with "; resp += ssid; resp += " WiFi network."; webserver->send(200, "text/plain", resp); delay(500); Serial.println("Disconnect from current WiFi network"); WiFi.disconnect(); } if (ssid.length() && pass.length()) { // Try to connect to new ssid Serial.print("\nConnecting to "); Serial.println(ssid); WiFi.begin(ssid.c_str(), pass.c_str()); uint32_t beginTime = millis(); while (WiFi.status() != WL_CONNECTED) { delay(100); Serial.print("*.*"); if (millis() - beginTime > m_timeout) break; } // reply to client if (WiFi.status() == WL_CONNECTED) { // WiFi.softAPdisconnect(); IPAddress ip = WiFi.localIP(); Serial.print("\nConnected to Wifi! IP address: "); Serial.println(ip); webserver->send(200, "text/plain", ip.toString()); m_apmode = false; delay(500); ESP.restart(); // Store current WiFi configuration in flash if (persistent) { #if defined(ESP8266) struct station_config stationConf; wifi_station_get_config_default(&stationConf); // Clear previuos configuration memset(&stationConf, 0, sizeof(stationConf)); os_memcpy(&stationConf.ssid, ssid.c_str(), ssid.length()); os_memcpy(&stationConf.password, pass.c_str(), pass.length()); wifi_set_opmode(STATION_MODE); wifi_station_set_config(&stationConf); #elif defined(ESP32) wifi_config_t stationConf; esp_wifi_get_config(WIFI_IF_STA, &stationConf); // Clear previuos configuration memset(&stationConf, 0, sizeof(stationConf)); memcpy(&stationConf.sta.ssid, ssid.c_str(), ssid.length()); memcpy(&stationConf.sta.password, pass.c_str(), pass.length()); esp_wifi_set_config(WIFI_IF_STA, &stationConf); #endif } else { #if defined(ESP8266) struct station_config stationConf; wifi_station_get_config_default(&stationConf); // Clear previuos configuration memset(&stationConf, 0, sizeof(stationConf)); wifi_station_set_config(&stationConf); #elif defined(ESP32) wifi_config_t stationConf; esp_wifi_get_config(WIFI_IF_STA, &stationConf); // Clear previuos configuration memset(&stationConf, 0, sizeof(stationConf)); esp_wifi_set_config(WIFI_IF_STA, &stationConf); #endif } } else webserver->send(500, "text/plain", "Connection error, maybe the password is wrong?"); } webserver->send(500, "text/plain", "Wrong credentials provided"); } void FSWebServer::setCrossOrigin() { webserver->sendHeader(F("Access-Control-Allow-Origin"), F("*")); webserver->sendHeader(F("Access-Control-Max-Age"), F("600")); webserver->sendHeader(F("Access-Control-Allow-Methods"), F("PUT,POST,GET,OPTIONS")); webserver->sendHeader(F("Access-Control-Allow-Headers"), F("*")); }; void FSWebServer::handleScanNetworks() { String jsonList = "["; DebugPrint("Scanning WiFi networks..."); int n = WiFi.scanNetworks(); DebugPrintln(" complete."); if (n == 0) { DebugPrintln("no networks found"); webserver->send(200, "text/json", "[]"); return; } else { DebugPrint(n); DebugPrintln(" networks found:"); for (int i = 0; i < n; ++i) { String ssid = WiFi.SSID(i); int rssi = WiFi.RSSI(i); #if defined(ESP8266) String security = WiFi.encryptionType(i) == AUTH_OPEN ? "none" : "enabled"; #elif defined(ESP32) String security = WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "none" : "enabled"; #endif jsonList += "{\"ssid\":\""; jsonList += ssid; jsonList += "\",\"strength\":\""; jsonList += rssi; jsonList += "\",\"security\":"; jsonList += security == "none" ? "false" : "true"; jsonList += ssid.equals(WiFi.SSID()) ? ",\"selected\": true" : ""; jsonList += i < n - 1 ? "}," : "}"; } jsonList += "]"; } webserver->send(200, "text/json", jsonList); DebugPrintln(jsonList); } #ifdef INCLUDE_SETUP_HTM void FSWebServer::addDropdownList(const char *label, const char **array, size_t size) { File file = m_filesystem->open("/config.json", "r"); int sz = file.size() * 1.33; int docSize = max(sz, 2048); DynamicJsonDocument doc((size_t)docSize); if (file) { // If file is present, load actual configuration DeserializationError error = deserializeJson(doc, file); if (error) { DebugPrintln(F("Failed to deserialize file, may be corrupted")); DebugPrintln(error.c_str()); file.close(); return; } file.close(); } else { DebugPrintln(F("File not found, will be created new configuration file")); } numOptions++; // If key is present in json, we don't need to create it. if (doc.containsKey(label)) return; JsonObject obj = doc.createNestedObject(label); obj["selected"] = array[0]; // first element selected as default JsonArray arr = obj.createNestedArray("values"); for (int i = 0; i < size; i++) { arr.add(array[i]); } file = m_filesystem->open("/config.json", "w"); if (serializeJsonPretty(doc, file) == 0) { DebugPrintln(F("Failed to write to file")); } file.close(); } void FSWebServer::removeWhiteSpaces(String &str) { const char noChars[] = {'\n', '\r', '\t'}; int pos = -1; // Remove non printable characters for (int i = 0; i < sizeof(noChars); i++) { pos = str.indexOf(noChars[i]); while (pos > -1) { str.replace(String(noChars[i]), ""); pos = str.indexOf(noChars[i]); } } // Remove doubles spaces pos = str.indexOf(" "); while (pos > -1) { str.replace(" ", " "); pos = str.indexOf(" "); } } void FSWebServer::handleSetup() { webserver->sendHeader(PSTR("Content-Encoding"), "gzip"); webserver->send_P(200, "text/html", SETUP_HTML, SETUP_HTML_SIZE); } #endif void FSWebServer::handleIndex() { if (m_filesystem->exists("/index.htm")) { handleFileRead("/index.htm"); } else if (m_filesystem->exists("/index.html")) { handleFileRead("/index.html"); } #ifdef INCLUDE_SETUP_HTM else { handleSetup(); } #endif } /* Read the given file from the filesystem and stream it back to the client */ bool FSWebServer::handleFileRead(const String &uri) { String path = m_basePath; path = uri; DebugPrintln("handleFileRead: " + path); if (path.endsWith("/")) { path += "index.htm"; } String pathWithGz = path + ".gz"; if (m_filesystem->exists(pathWithGz) || m_filesystem->exists(path)) { if (m_filesystem->exists(pathWithGz)) { path += ".gz"; } const char *contentType = getContentType(path.c_str()); File file = m_filesystem->open(path, "r"); if (webserver->streamFile(file, contentType) != file.size()) { DebugPrintln(PSTR("Sent less data than expected!")); // webserver->stop(); } file.close(); return true; } return false; } /* Handle a file upload request */ void FSWebServer::handleFileUpload() { if (webserver->uri() != "/edit") { return; } HTTPUpload &upload = webserver->upload(); if (upload.status == UPLOAD_FILE_START) { String filename = upload.filename; String result; // Make sure paths always start with "/" if (!filename.startsWith("/")) { filename = "/" + filename; } checkForUnsupportedPath(filename, result); if (result.length() > 0) { replyToCLient(ERROR, PSTR("INVALID FILENAME")); return; } DebugPrintf_P(PSTR("handleFileUpload Name: %s\n"), filename.c_str()); m_uploadFile = m_filesystem->open(filename, "w"); if (!m_uploadFile) { replyToCLient(ERROR, PSTR("CREATE FAILED")); return; } DebugPrintf_P(PSTR("Upload: START, filename: %s\n"), filename.c_str()); } else if (upload.status == UPLOAD_FILE_WRITE) { if (m_uploadFile) { size_t bytesWritten = m_uploadFile.write(upload.buf, upload.currentSize); if (bytesWritten != upload.currentSize) { replyToCLient(ERROR, PSTR("WRITE FAILED")); return; } } DebugPrintf_P(PSTR("Upload: WRITE, Bytes: %d\n"), upload.currentSize); } else if (upload.status == UPLOAD_FILE_END) { if (m_uploadFile) { m_uploadFile.close(); } DebugPrintf_P(PSTR("Upload: END, Size: %d\n"), upload.totalSize); } } void FSWebServer::replyOK() { replyToCLient(OK, ""); } void FSWebServer::replyToCLient(int msg_type = 0, const char *msg = "") { webserver->sendHeader("Access-Control-Allow-Origin", "*"); switch (msg_type) { case OK: webserver->send(200, FPSTR(TEXT_PLAIN), ""); break; case CUSTOM: webserver->send(200, FPSTR(TEXT_PLAIN), msg); break; case NOT_FOUND: webserver->send(404, FPSTR(TEXT_PLAIN), msg); break; case BAD_REQUEST: webserver->send(400, FPSTR(TEXT_PLAIN), msg); break; case ERROR: webserver->send(500, FPSTR(TEXT_PLAIN), msg); break; } } /* Checks filename for character combinations that are not supported by FSBrowser (alhtough valid on SPIFFS). Returns an empty String if supported, or detail of error(s) if unsupported */ void FSWebServer::checkForUnsupportedPath(String &filename, String &error) { if (!filename.startsWith("/")) { error += PSTR(" !! NO_LEADING_SLASH !! "); } if (filename.indexOf("//") != -1) { error += PSTR(" !! DOUBLE_SLASH !! "); } if (filename.endsWith("/")) { error += PSTR(" ! TRAILING_SLASH ! "); } DebugPrintln(filename); DebugPrintln(error); } const char *FSWebServer::getContentType(const char *filename) { if (webserver->hasArg("download")) return PSTR("application/octet-stream"); else if (strstr(filename, ".htm")) return PSTR("text/html"); else if (strstr(filename, ".html")) return PSTR("text/html"); else if (strstr(filename, ".css")) return PSTR("text/css"); else if (strstr(filename, ".sass")) return PSTR("text/css"); else if (strstr(filename, ".js")) return PSTR("application/javascript"); else if (strstr(filename, ".png")) return PSTR("image/png"); else if (strstr(filename, ".svg")) return PSTR("image/svg+xml"); else if (strstr(filename, ".gif")) return PSTR("image/gif"); else if (strstr(filename, ".jpg")) return PSTR("image/jpeg"); else if (strstr(filename, ".ico")) return PSTR("image/x-icon"); else if (strstr(filename, ".xml")) return PSTR("text/xml"); else if (strstr(filename, ".pdf")) return PSTR("application/x-pdf"); else if (strstr(filename, ".zip")) return PSTR("application/x-zip"); else if (strstr(filename, ".gz")) return PSTR("application/x-gzip"); return PSTR("text/plain"); } // edit page, in usefull in some situation, but if you need to provide only a web interface, you can disable #ifdef INCLUDE_EDIT_HTM /* Return the list of files in the directory specified by the "dir" query string parameter. Also demonstrates the use of chuncked responses. */ void FSWebServer::handleFileList() { if (!webserver->hasArg("dir")) { return replyToCLient(BAD_REQUEST, "DIR ARG MISSING"); } String path = webserver->arg("dir"); DebugPrintln("handleFileList: " + path); if (path != "/" && !m_filesystem->exists(path)) { return replyToCLient(BAD_REQUEST, "BAD PATH"); } File root = m_filesystem->open(path, "r"); path = String(); String output; output.reserve(256); output = "["; if (root.isDirectory()) { File file = root.openNextFile(); while (file) { String filename = file.name(); if (filename.lastIndexOf("/") > -1) { filename.remove(0, filename.lastIndexOf("/") + 1); } if (output != "[") { output += ','; } output += "{\"type\":\""; output += (file.isDirectory()) ? "dir" : "file"; output += "\",\"size\":\""; output += file.size(); output += "\",\"name\":\""; output += filename; output += "\"}"; file = root.openNextFile(); } } output += "]"; webserver->send(200, "text/json", output); } /* Handle the creation/rename of a new file Operation | req.responseText ---------------+-------------------------------------------------------------- Create file | parent of created file Create folder | parent of created folder Rename file | parent of source file Move file | parent of source file, or remaining ancestor Rename folder | parent of source folder Move folder | parent of source folder, or remaining ancestor */ void FSWebServer::handleFileCreate() { String path = webserver->arg("path"); if (path.isEmpty()) { replyToCLient(BAD_REQUEST, PSTR("PATH ARG MISSING")); return; } if (path == "/") { replyToCLient(BAD_REQUEST, PSTR("BAD PATH")); return; } String src = webserver->arg("src"); if (src.isEmpty()) { // No source specified: creation DebugPrintf_P(PSTR("handleFileCreate: %s\n"), path.c_str()); if (path.endsWith("/")) { // Create a folder path.remove(path.length() - 1); if (!m_filesystem->mkdir(path)) { replyToCLient(ERROR, PSTR("MKDIR FAILED")); return; } } else { // Create a file File file = m_filesystem->open(path, "w"); if (file) { file.write(0); file.close(); } else { replyToCLient(ERROR, PSTR("CREATE FAILED")); return; } } replyToCLient(CUSTOM, path.c_str()); } else { // Source specified: rename if (src == "/") { replyToCLient(BAD_REQUEST, PSTR("BAD SRC")); return; } if (!m_filesystem->exists(src)) { replyToCLient(BAD_REQUEST, PSTR("BSRC FILE NOT FOUND")); return; } DebugPrintf_P(PSTR("handleFileCreate: %s from %s\n"), path.c_str(), src.c_str()); if (path.endsWith("/")) { path.remove(path.length() - 1); } if (src.endsWith("/")) { src.remove(src.length() - 1); } if (!m_filesystem->rename(src, path)) { replyToCLient(ERROR, PSTR("RENAME FAILED")); return; } replyOK(); } } /* Handle a file deletion request Operation | req.responseText ---------------+-------------------------------------------------------------- Delete file | parent of deleted file, or remaining ancestor Delete folder | parent of deleted folder, or remaining ancestor */ void FSWebServer::handleFileDelete() { String path = webserver->arg(0); if (path.isEmpty() || path == "/") { replyToCLient(BAD_REQUEST, PSTR("BAD PATH")); return; } DebugPrintf_P(PSTR("handleFileDelete: %s\n"), path.c_str()); if (!m_filesystem->exists(path)) { replyToCLient(NOT_FOUND, PSTR(FILE_NOT_FOUND)); return; } // deleteRecursive(path); File root = m_filesystem->open(path, "r"); // If it's a plain file, delete it if (!root.isDirectory()) { root.close(); m_filesystem->remove(path); replyOK(); } else { m_filesystem->rmdir(path); replyOK(); } } /* This specific handler returns the edit.htm (or a gzipped version) from the /edit folder. If the file is not present but the flag INCLUDE_FALLBACK_INDEX_HTM has been set, falls back to the version embedded in the program code. Otherwise, fails with a 404 page with debug information */ void FSWebServer::handleGetEdit() { #ifdef INCLUDE_EDIT_HTM webserver->sendHeader(PSTR("Content-Encoding"), "gzip"); webserver->send_P(200, "text/html", edit_htm_gz, sizeof(edit_htm_gz)); #else replyToCLient(NOT_FOUND, PSTR("FILE_NOT_FOUND")); #endif } /* Return the FS type, status and size info */ void FSWebServer::handleStatus() { DebugPrintln(PSTR("handleStatus")); size_t totalBytes = 1024; size_t usedBytes = 0; #ifdef ESP8266 FSInfo fs_info; m_filesystem->info(fs_info); totalBytes = fs_info.totalBytes; usedBytes = fs_info.usedBytes; #elif defined(ESP32) totalBytes = LittleFS.totalBytes(); usedBytes = LittleFS.usedBytes(); #endif String json; json.reserve(128); json = "{\"type\":\"Filesystem\", \"isOk\":"; if (m_fsOK) { json += PSTR("\"true\", \"totalBytes\":\""); json += totalBytes; json += PSTR("\", \"usedBytes\":\""); json += usedBytes; json += "\""; } else json += "\"false\""; json += PSTR(",\"unsupportedFiles\":\"\"}"); webserver->send(200, "application/json", json); } #endif // INCLUDE_EDIT_HTM