//#define PORTAL_CACHE "max-age=86400" #define PORTAL_CACHE nullptr #include #include #include #include #include #include #include using namespace NetworkUtils; extern NetworkMgr* network; extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings; extern MqttTask* tMqtt; class PortalTask : public LeanTask { public: PortalTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) { this->webServer = new AsyncWebServer(80); this->dnsServer = new DNSServer(); } ~PortalTask() { if (this->webServer != nullptr) { this->stopWebServer(); delete this->webServer; } if (this->dnsServer != nullptr) { this->stopDnsServer(); delete this->dnsServer; } } protected: const unsigned int changeStateInterval = 5000; AsyncWebServer* webServer = nullptr; DNSServer* dnsServer = nullptr; bool webServerEnabled = false; bool dnsServerEnabled = false; unsigned long webServerChangeState = 0; bool mDnsState = false; #if defined(ARDUINO_ARCH_ESP32) const char* getTaskName() override { return "Portal"; } /*BaseType_t getTaskCore() override { return 1; }*/ int getTaskPriority() override { return 1; } #endif void setup() { this->dnsServer->setTTL(0); this->dnsServer->setErrorReplyCode(DNSReplyCode::NoError); // auth middleware static AsyncMiddlewareFunction authMiddleware([](AsyncWebServerRequest *request, ArMiddlewareNext next) { if (network->isApEnabled() || !settings.portal.auth || !strlen(settings.portal.password)) { return next(); } if (request->authenticate(settings.portal.login, settings.portal.password, PROJECT_NAME)) { return next(); } return request->requestAuthentication(AsyncAuthType::AUTH_BASIC, PROJECT_NAME, "Authentication failed"); }); // index page this->webServer->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->redirect("/index.html"); }); this->webServer->serveStatic("/index.html", LittleFS, "/pages/index.html", PORTAL_CACHE); // dashboard page this->webServer->serveStatic("/dashboard.html", LittleFS, "/pages/dashboard.html", PORTAL_CACHE) .addMiddleware(&authMiddleware); // restart this->webServer->on("/restart.html", HTTP_GET, [](AsyncWebServerRequest *request) { vars.actions.restart = true; request->redirect("/"); }).addMiddleware(&authMiddleware); // network settings page this->webServer->serveStatic("/network.html", LittleFS, "/pages/network.html", PORTAL_CACHE) .addMiddleware(&authMiddleware); // settings page this->webServer->serveStatic("/settings.html", LittleFS, "/pages/settings.html", PORTAL_CACHE) .addMiddleware(&authMiddleware); // sensors page this->webServer->serveStatic("/sensors.html", LittleFS, "/pages/sensors.html", PORTAL_CACHE) .addMiddleware(&authMiddleware); // upgrade page this->webServer->serveStatic("/upgrade.html", LittleFS, "/pages/upgrade.html", PORTAL_CACHE) .addMiddleware(&authMiddleware); // OTA auto upgradeHandler = (new UpgradeHandler("/api/upgrade")) ->setBeforeUpgradeCallback([](AsyncWebServerRequest *request, UpgradeHandler::UpgradeType type) -> bool { if (vars.states.restarting) { return false; } vars.states.upgrading = true; return true; })->setAfterUpgradeCallback([](AsyncWebServerRequest *request, const UpgradeHandler::UpgradeResult& fwResult, const UpgradeHandler::UpgradeResult& fsResult) { unsigned short status = 406; if (fwResult.status == UpgradeHandler::UpgradeStatus::SUCCESS || fsResult.status == UpgradeHandler::UpgradeStatus::SUCCESS) { status = 202; vars.actions.restart = true; } vars.states.upgrading = false; // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); auto fwDoc = doc["firmware"].to(); fwDoc["status"] = static_cast(fwResult.status); fwDoc["error"] = fwResult.error; auto fsDoc = doc["filesystem"].to(); fsDoc["status"] = static_cast(fsResult.status); fsDoc["error"] = fsResult.error; // send response response->setLength(); response->setCode(status); request->send(response); }); this->webServer->addHandler(upgradeHandler); // backup this->webServer->on("/api/backup/save", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); // add network settings auto networkDoc = doc[FPSTR(S_NETWORK)].to(); networkSettingsToJson(networkSettings, networkDoc); // add main settings auto settingsDoc = doc[FPSTR(S_SETTINGS)].to(); settingsToJson(settings, settingsDoc); // add sensors settings for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { auto sensorsettingsDoc = doc[FPSTR(S_SENSORS)][sensorId].to(); sensorSettingsToJson(sensorId, Sensors::settings[sensorId], sensorsettingsDoc); } // send response response->addHeader("Content-Disposition", "attachment; filename=\"backup.json\""); response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); // backup restore auto& brHandler = this->webServer->on("/api/backup/restore", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &requestDoc) { if (vars.states.restarting) { return request->send(503); } if (requestDoc.isNull() || !requestDoc.size()) { return request->send(400); } // prepare request bool changed = false; // restore network settings if (!requestDoc[FPSTR(S_NETWORK)].isNull() && jsonToNetworkSettings(requestDoc[FPSTR(S_NETWORK)], networkSettings)) { fsNetworkSettings.update(); network->setHostname(networkSettings.hostname) ->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel) ->setApCredentials(networkSettings.ap.ssid, networkSettings.ap.password, networkSettings.ap.channel) ->setUseDhcp(networkSettings.useDhcp) ->setStaticConfig( networkSettings.staticConfig.ip, networkSettings.staticConfig.gateway, networkSettings.staticConfig.subnet, networkSettings.staticConfig.dns ); changed = true; } // restore main settings if (!requestDoc[FPSTR(S_SETTINGS)].isNull() && jsonToSettings(requestDoc[FPSTR(S_SETTINGS)], settings)) { fsSettings.update(); changed = true; } // restore sensors settings if (!requestDoc[FPSTR(S_SENSORS)].isNull()) { for (auto sensor : requestDoc[FPSTR(S_SENSORS)].as()) { if (!isDigit(sensor.key().c_str())) { continue; } int sensorId = atoi(sensor.key().c_str()); if (sensorId < 0 || sensorId > 255 || !Sensors::isValidSensorId(sensorId)) { continue; } if (jsonToSensorSettings(sensorId, sensor.value(), Sensors::settings[sensorId])) { fsSensorsSettings.update(); changed = true; } } } requestDoc.clear(); // send response request->send(changed ? 201 : 200); if (changed) { vars.actions.restart = true; } }); brHandler.setMaxContentLength(3072); brHandler.addMiddleware(&authMiddleware); // network this->webServer->on("/api/network/settings", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); networkSettingsToJson(networkSettings, doc); // send response response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); auto& nsHandler = this->webServer->on("/api/network/settings", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &requestDoc) { if (vars.states.restarting) { return request->send(503); } if (requestDoc.isNull() || !requestDoc.size()) { return request->send(400); } // prepare request bool changed = jsonToNetworkSettings(requestDoc, networkSettings); requestDoc.clear(); // make response auto *response = new AsyncJsonResponse(); auto& responseDoc = response->getRoot(); networkSettingsToJson(networkSettings, responseDoc); // send response response->setLength(); response->setCode(changed ? 201 : 200); request->send(response); if (changed) { fsNetworkSettings.update(); network->setHostname(networkSettings.hostname) ->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel) ->setApCredentials(networkSettings.ap.ssid, networkSettings.ap.password, networkSettings.ap.channel) ->setUseDhcp(networkSettings.useDhcp) ->setStaticConfig( networkSettings.staticConfig.ip, networkSettings.staticConfig.gateway, networkSettings.staticConfig.subnet, networkSettings.staticConfig.dns ) ->reconnect(); } }); nsHandler.setMaxContentLength(512); nsHandler.addMiddleware(&authMiddleware); this->webServer->on("/api/network/scan", HTTP_GET, [](AsyncWebServerRequest *request) { auto apCount = WiFi.scanComplete(); switch (apCount) { case WIFI_SCAN_FAILED: WiFi.scanNetworks(true, true, false); request->send(404); break; case WIFI_SCAN_RUNNING: request->send(202); break; default: // make respponse auto *response = new AsyncJsonResponse(true); auto& doc = response->getRoot(); for (short int i = 0; i < apCount; i++) { doc[i][FPSTR(S_SSID)] = WiFi.SSID(i); doc[i][FPSTR(S_BSSID)] = WiFi.BSSIDstr(i); doc[i][FPSTR(S_SIGNAL_QUALITY)] = NetworkMgr::rssiToSignalQuality(WiFi.RSSI(i)); doc[i][FPSTR(S_CHANNEL)] = WiFi.channel(i); doc[i][FPSTR(S_HIDDEN)] = !WiFi.SSID(i).length(); doc[i][FPSTR(S_AUTH)] = WiFi.encryptionType(i); } // send response response->setLength(); request->send(response); // clear WiFi.scanDelete(); } }).addMiddleware(&authMiddleware); // settings this->webServer->on("/api/settings", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); settingsToJson(settings, doc); // send response response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); auto& sHandler = this->webServer->on("/api/settings", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &requestDoc) { if (vars.states.restarting) { return request->send(503); } if (requestDoc.isNull() || !requestDoc.size()) { return request->send(400); } // prepare request bool changed = jsonToSettings(requestDoc, settings); requestDoc.clear(); // make response auto *response = new AsyncJsonResponse(); auto& responseDoc = response->getRoot(); settingsToJson(settings, responseDoc); // send response response->setLength(); response->setCode(changed ? 201 : 200); request->send(response); if (changed) { fsSettings.update(); tMqtt->resetPublishedSettingsTime(); } }); sHandler.setMaxContentLength(3072); sHandler.addMiddleware(&authMiddleware); // sensors list this->webServer->on("/api/sensors", HTTP_GET, [](AsyncWebServerRequest *request) { // make response bool detailed = request->hasParam("detailed"); auto *response = new AsyncJsonResponse(true); auto& responseDoc = response->getRoot(); // add sensors for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { if (detailed) { auto& sSensor = Sensors::settings[sensorId]; responseDoc[sensorId][FPSTR(S_ENABLED)] = sSensor.enabled; responseDoc[sensorId][FPSTR(S_NAME)] = sSensor.name; responseDoc[sensorId][FPSTR(S_PURPOSE)] = static_cast(sSensor.purpose); sensorResultToJson(sensorId, responseDoc[sensorId]); } else { responseDoc[sensorId] = Sensors::settings[sensorId].name; } } // send response response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); // sensor settings this->webServer->on("/api/sensor", HTTP_GET, [](AsyncWebServerRequest *request) { if (!request->hasParam("id")) { return request->send(400); } const auto& id = request->getParam("id")->value(); if (!isDigit(id.c_str())) { return request->send(400); } uint8_t sensorId = id.toInt(); if (!Sensors::isValidSensorId(sensorId)) { return request->send(404); } auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); sensorSettingsToJson(sensorId, Sensors::settings[sensorId], doc); response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); auto& ssHandler = this->webServer->on("/api/sensor", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &requestDoc) { if (vars.states.restarting) { return request->send(503); } if (requestDoc.isNull() || !requestDoc.size()) { return request->send(400); } if (!request->hasParam("id")) { return request->send(400); } auto& id = request->getParam("id")->value(); if (!isDigit(id.c_str())) { return request->send(400); } uint8_t sensorId = id.toInt(); if (!Sensors::isValidSensorId(sensorId)) { return request->send(404); } // prepare request auto prevSettings = Sensors::settings[sensorId]; bool changed = jsonToSensorSettings(sensorId, requestDoc, Sensors::settings[sensorId]); requestDoc.clear(); // make response auto *response = new AsyncJsonResponse(); auto& responseDoc = response->getRoot(); auto& sSettings = Sensors::settings[sensorId]; sensorSettingsToJson(sensorId, sSettings, responseDoc); // send response response->setLength(); response->setCode(changed ? 201 : 200); request->send(response); if (changed) { tMqtt->reconfigureSensor(sensorId, prevSettings); fsSensorsSettings.update(); } }); ssHandler.setMaxContentLength(1024); ssHandler.addMiddleware(&authMiddleware); // vars this->webServer->on("/api/vars", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); varsToJson(vars, doc); // send response response->setLength(); request->send(response); }); auto& vHandler = this->webServer->on("/api/vars", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &requestDoc) { if (vars.states.restarting) { return request->send(503); } if (requestDoc.isNull() || !requestDoc.size()) { return request->send(400); } // prepare request bool changed = jsonToVars(requestDoc, vars); requestDoc.clear(); // make response auto *response = new AsyncJsonResponse(); auto& responseDoc = response->getRoot(); varsToJson(vars, responseDoc); // send response response->setLength(); response->setCode(changed ? 201 : 200); request->send(response); if (changed) { tMqtt->resetPublishedVarsTime(); } }); vHandler.setMaxContentLength(1536); vHandler.addMiddleware(&authMiddleware); this->webServer->on("/api/info", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); auto docSystem = doc[FPSTR(S_SYSTEM)].to(); docSystem[FPSTR(S_RESET_REASON)] = getResetReason(); docSystem[FPSTR(S_UPTIME)] = millis() / 1000; bool isConnected = network->isConnected(); auto docNetwork = doc[FPSTR(S_NETWORK)].to(); docNetwork[FPSTR(S_HOSTNAME)] = networkSettings.hostname; docNetwork[FPSTR(S_MAC)] = network->getStaMac(); docNetwork[FPSTR(S_CONNECTED)] = isConnected; docNetwork[FPSTR(S_SSID)] = network->getStaSsid(); docNetwork[FPSTR(S_SIGNAL_QUALITY)] = isConnected ? NetworkMgr::rssiToSignalQuality(network->getRssi()) : 0; docNetwork[FPSTR(S_CHANNEL)] = isConnected ? network->getStaChannel() : 0; docNetwork[FPSTR(S_IP)] = isConnected ? network->getStaIp().toString() : ""; docNetwork[FPSTR(S_SUBNET)] = isConnected ? network->getStaSubnet().toString() : ""; docNetwork[FPSTR(S_GATEWAY)] = isConnected ? network->getStaGateway().toString() : ""; docNetwork[FPSTR(S_DNS)] = isConnected ? network->getStaDns().toString() : ""; auto docBuild = doc[FPSTR(S_BUILD)].to(); docBuild[FPSTR(S_VERSION)] = BUILD_VERSION; docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__; docBuild[FPSTR(S_ENV)] = BUILD_ENV; docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); auto docHeap = doc[FPSTR(S_HEAP)].to(); docHeap[FPSTR(S_TOTAL)] = getTotalHeap(); docHeap[FPSTR(S_FREE)] = getFreeHeap(); docHeap[FPSTR(S_MIN_FREE)] = getFreeHeap(true); docHeap[FPSTR(S_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(); docHeap[FPSTR(S_MIN_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(true); auto docPsram = doc[FPSTR(S_PSRAM)].to(); docPsram[FPSTR(S_TOTAL)] = ESP.getPsramSize(); docPsram[FPSTR(S_FREE)] = ESP.getFreePsram(); docPsram[FPSTR(S_MIN_FREE)] = ESP.getMinFreePsram(); docPsram[FPSTR(S_MAX_FREE_BLOCK)] = ESP.getMaxAllocPsram(); auto docChip = doc[FPSTR(S_CHIP)].to(); docChip[FPSTR(S_MODEL)] = ESP.getChipModel(); docChip[FPSTR(S_REV)] = ESP.getChipRevision(); docChip[FPSTR(S_CORES)] = ESP.getChipCores(); docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); auto docFlash = doc[FPSTR(S_FLASH)].to(); docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); docFlash[FPSTR(S_REAL_SIZE)] = docFlash[FPSTR(S_SIZE)]; // send response response->setLength(); request->send(response); }); this->webServer->on("/api/debug", HTTP_GET, [](AsyncWebServerRequest *request) { // make response auto *response = new AsyncJsonResponse(); auto& doc = response->getRoot(); auto docBuild = doc[FPSTR(S_BUILD)].to(); docBuild[FPSTR(S_VERSION)] = BUILD_VERSION; docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__; docBuild[FPSTR(S_ENV)] = BUILD_ENV; docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); auto docHeap = doc[FPSTR(S_HEAP)].to(); docHeap[FPSTR(S_TOTAL)] = getTotalHeap(); docHeap[FPSTR(S_FREE)] = getFreeHeap(); docHeap[FPSTR(S_MIN_FREE)] = getFreeHeap(true); docHeap[FPSTR(S_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(); docHeap[FPSTR(S_MIN_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(true); auto docChip = doc[FPSTR(S_CHIP)].to(); docChip[FPSTR(S_MODEL)] = ESP.getChipModel(); docChip[FPSTR(S_REV)] = ESP.getChipRevision(); docChip[FPSTR(S_CORES)] = ESP.getChipCores(); docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); auto docFlash = doc[FPSTR(S_FLASH)].to(); docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); docFlash[FPSTR(S_REAL_SIZE)] = docFlash[FPSTR(S_SIZE)]; auto reason = esp_reset_reason(); if (reason != ESP_RST_UNKNOWN && reason != ESP_RST_POWERON && reason != ESP_RST_SW) { auto docCrash = doc[FPSTR(S_CRASH)].to(); docCrash[FPSTR(S_REASON)] = getResetReason(); docCrash[FPSTR(S_CORE)] = CrashRecorder::ext.core; docCrash[FPSTR(S_HEAP)] = CrashRecorder::ext.heap; docCrash[FPSTR(S_UPTIME)] = CrashRecorder::ext.uptime; if (CrashRecorder::backtrace.length > 0 && CrashRecorder::backtrace.length <= CrashRecorder::backtraceMaxLength) { String backtraceStr; arr2str(backtraceStr, CrashRecorder::backtrace.data, CrashRecorder::backtrace.length); docCrash[FPSTR(S_BACKTRACE)][FPSTR(S_DATA)] = backtraceStr; docCrash[FPSTR(S_BACKTRACE)][FPSTR(S_CONTINUES)] = CrashRecorder::backtrace.continues; } if (CrashRecorder::epc.length > 0 && CrashRecorder::epc.length <= CrashRecorder::epcMaxLength) { String epcStr; arr2str(epcStr, CrashRecorder::epc.data, CrashRecorder::epc.length); docCrash[FPSTR(S_EPC)] = epcStr; } } // send response response->addHeader("Content-Disposition", "attachment; filename=\"debug.json\""); response->setLength(); request->send(response); }).addMiddleware(&authMiddleware); // not found this->webServer->onNotFound([this](AsyncWebServerRequest *request) { const auto& url = request->url(); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Page not found, uri: %s"), url.c_str()); if (url.equals("/")) { request->send(200, "text/plain", "The file system is not flashed!"); } else if (network->isApEnabled()) { this->onCaptivePortal(request); } else { request->send(404, "text/plain", "Page not found"); } }); this->webServer->serveStatic("/robots.txt", LittleFS, "/static/robots.txt", PORTAL_CACHE); this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/images/favicon.ico", PORTAL_CACHE); this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE); } void loop() { // web server if (!this->stateWebServer() && (network->isApEnabled() || network->isConnected()) && millis() - this->webServerChangeState >= this->changeStateInterval) { this->startWebServer(); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected")); // Enabling mDNS if (!this->mDnsState && settings.portal.mdns) { if (MDNS.begin(networkSettings.hostname)) { MDNS.addService("http", "tcp", 80); this->mDnsState = true; Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS enabled and service added")); } } } else if (this->stateWebServer() && !network->isApEnabled() && !network->isStaEnabled()) { this->stopWebServer(); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down")); // Disabling mDNS if (this->mDnsState) { MDNS.end(); this->mDnsState = false; Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS disabled")); } } // Disabling mDNS if disabled in settings if (this->mDnsState && !settings.portal.mdns) { MDNS.end(); this->mDnsState = false; Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS disabled")); } // dns server if (!this->stateDnsServer() && !network->isConnected() && network->isApEnabled() && this->stateWebServer()) { this->startDnsServer(); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up")); } else if (this->stateDnsServer() && (network->isConnected() || !network->isApEnabled() || !this->stateWebServer())) { this->stopDnsServer(); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down/STA connected")); } if (this->stateDnsServer()) { this->dnsServer->processNextRequest(); } if (!this->stateDnsServer()) { this->delay(250); } } void onCaptivePortal(AsyncWebServerRequest *request) { const auto& url = request->url(); if (url.equals("/connecttest.txt")) { request->redirect("http://logout.net", 302); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to http://logout.net with 302 code")); } else if (url.equals(F("/wpad.dat"))) { request->send(404); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 404 code")); } else if (url.equals(F("/success.txt"))) { request->send(200); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 200 code")); } else { String portalUrl = "http://"; portalUrl += network->getApIp().toString().c_str(); portalUrl += '/'; request->redirect(portalUrl, 302); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to portal page with 302 code")); } } bool stateWebServer() { return this->webServerEnabled; } void startWebServer() { if (this->stateWebServer()) { return; } this->webServer->begin(); this->webServerEnabled = true; this->webServerChangeState = millis(); } void stopWebServer() { if (!this->stateWebServer()) { return; } this->webServer->end(); this->webServerEnabled = false; this->webServerChangeState = millis(); } bool stateDnsServer() { return this->dnsServerEnabled; } void startDnsServer() { if (this->stateDnsServer()) { return; } this->dnsServer->start(53, "*", network->getApIp()); this->dnsServerEnabled = true; } void stopDnsServer() { if (!this->stateDnsServer()) { return; } //this->dnsServer->processNextRequest(); this->dnsServer->stop(); this->dnsServerEnabled = false; } };