| 2 |
raymond |
1 |
#include "AsyncFsWebServer.h"
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
void setTaskWdt(uint32_t timeout) {
|
|
|
5 |
#if defined(ESP32)
|
|
|
6 |
#if ESP_ARDUINO_VERSION_MAJOR > 2
|
|
|
7 |
esp_task_wdt_config_t twdt_config = {
|
|
|
8 |
.timeout_ms = timeout,
|
|
|
9 |
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // Bitmask of all cores
|
|
|
10 |
.trigger_panic = false,
|
|
|
11 |
};
|
|
|
12 |
ESP_ERROR_CHECK(esp_task_wdt_reconfigure(&twdt_config));
|
|
|
13 |
#else
|
|
|
14 |
ESP_ERROR_CHECK(esp_task_wdt_init(timeout / 1000, 0));
|
|
|
15 |
#endif
|
|
|
16 |
#elif defined(ESP8266)
|
|
|
17 |
ESP.wdtDisable();
|
|
|
18 |
ESP.wdtEnable(timeout);
|
|
|
19 |
#endif
|
|
|
20 |
}
|
|
|
21 |
|
|
|
22 |
|
|
|
23 |
bool AsyncFsWebServer::init(AwsEventHandler wsHandle) {
|
|
|
24 |
// Set build date as default firmware version (YYMMDDHHmm) from Version.h constexprs
|
|
|
25 |
if (m_version.length() == 0)
|
|
|
26 |
m_version = String(BUILD_TIMESTAMP);
|
|
|
27 |
|
|
|
28 |
////////////////////// BUILT-IN HANDLERS ////////////////////////////
|
|
|
29 |
on("*", HTTP_HEAD, [this](AsyncWebServerRequest *request) { this->handleFileName(request); });
|
|
|
30 |
|
|
|
31 |
#if ESP_FS_WS_SETUP
|
|
|
32 |
m_filesystem_ok = getSetupConfigurator()->checkConfigFile();
|
|
|
33 |
if (getSetupConfigurator()->isOpened()) {
|
|
|
34 |
log_debug("Config file %s closed", ESP_FS_WS_CONFIG_FILE);
|
|
|
35 |
getSetupConfigurator()->closeConfiguration();
|
|
|
36 |
}
|
|
|
37 |
onUpdate();
|
|
|
38 |
|
|
|
39 |
// Handler for serving the isolated credentials script (Gzipped from PROGMEM)
|
|
|
40 |
on("/creds.js", HTTP_GET, [this](AsyncWebServerRequest *request) {
|
|
|
41 |
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (uint8_t*)_accreds_js, sizeof(_accreds_js));
|
|
|
42 |
response->addHeader("Content-Encoding", "gzip");
|
|
|
43 |
response->addHeader("X-Config-File", ESP_FS_WS_CONFIG_FILE);
|
|
|
44 |
response->addHeader("Cache-Control", "public, max-age=86400");
|
|
|
45 |
request->send(response);
|
|
|
46 |
});
|
|
|
47 |
|
|
|
48 |
on("/setup", HTTP_GET, [this](AsyncWebServerRequest *request) { this->handleSetup(request); });
|
|
|
49 |
|
|
|
50 |
// Serve default logo from PROGMEM when no custom logo exists on filesystem
|
|
|
51 |
on("/config/logo.svg", HTTP_GET, [this](AsyncWebServerRequest *request) {
|
|
|
52 |
if (!m_filesystem->exists("/config/logo.svg") &&
|
|
|
53 |
!m_filesystem->exists("/config/logo.svg.gz") &&
|
|
|
54 |
!m_filesystem->exists("/config/logo.png") &&
|
|
|
55 |
!m_filesystem->exists("/config/logo.jpg") &&
|
|
|
56 |
!m_filesystem->exists("/config/logo.gif")) {
|
|
|
57 |
AsyncWebServerResponse *response = request->beginResponse(200, "image/svg+xml", (uint8_t*)_aclogo_svg, sizeof(_aclogo_svg));
|
|
|
58 |
response->addHeader("Content-Encoding", "gzip");
|
|
|
59 |
response->addHeader("Cache-Control", "public, max-age=86400");
|
|
|
60 |
request->send(response);
|
|
|
61 |
} else {
|
|
|
62 |
request->send(404); // Let serveStatic handle it
|
|
|
63 |
}
|
|
|
64 |
});
|
|
|
65 |
on("/connect", HTTP_POST, [this](AsyncWebServerRequest *request) { this->doWifiConnection(request); });
|
|
|
66 |
on("/scan", HTTP_GET, [this](AsyncWebServerRequest *request) { this->handleScanNetworks(request); });
|
|
|
67 |
on("/getStatus", HTTP_GET, [this](AsyncWebServerRequest *request) { this->getStatus(request); });
|
|
|
68 |
on("/clear_config", HTTP_GET, [this](AsyncWebServerRequest *request) { this->clearConfig(request); });
|
|
|
69 |
// Simple WiFi status endpoint
|
|
|
70 |
on(AsyncURIMatcher::exact("/wifi"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
|
71 |
CJSON::Json doc;
|
|
|
72 |
doc.setString("ssid", WiFi.SSID());
|
|
|
73 |
doc.setNumber("rssi", WiFi.RSSI());
|
|
|
74 |
request->send(200, "application/json", doc.serialize());
|
|
|
75 |
});
|
|
|
76 |
// File upload handler for configuration files
|
|
|
77 |
on("/upload", HTTP_POST,
|
|
|
78 |
[this](AsyncWebServerRequest *request) { this->sendOK(request); },
|
|
|
79 |
[this](AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
|
80 |
this->handleUpload(request, filename, index, data, len, final);
|
|
|
81 |
}
|
|
|
82 |
);
|
|
|
83 |
// Endpoint to reset device
|
|
|
84 |
on("/reset", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
|
85 |
// Send response and restart AFTER client disconnects to ensure 200 reaches browser
|
|
|
86 |
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", WiFi.localIP().toString());
|
|
|
87 |
response->addHeader("Connection", "close");
|
|
|
88 |
request->onDisconnect([]() {
|
|
|
89 |
#if defined(ESP8266)
|
|
|
90 |
ESP.reset();
|
|
|
91 |
#else
|
|
|
92 |
ESP.restart();
|
|
|
93 |
#endif
|
|
|
94 |
});
|
|
|
95 |
request->send(response);
|
|
|
96 |
});
|
|
|
97 |
|
|
|
98 |
// WiFi credentials management (no plaintext passwords exposed)
|
|
|
99 |
on(AsyncURIMatcher::exact("/wifi/credentials"), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
|
|
100 |
CJSON::Json json_array;
|
|
|
101 |
json_array.createArray();
|
|
|
102 |
if (m_credentialManager) {
|
|
|
103 |
std::vector<WiFiCredential>* creds = m_credentialManager->getCredentials();
|
|
|
104 |
if (creds) {
|
|
|
105 |
for (size_t i = 0; i < creds->size(); ++i) {
|
|
|
106 |
const WiFiCredential &c = (*creds)[i];
|
|
|
107 |
CJSON::Json item;
|
|
|
108 |
item.setNumber("index", static_cast<int>(i));
|
|
|
109 |
item.setString("ssid", c.ssid);
|
|
|
110 |
|
|
|
111 |
// Static network configuration (if present)
|
|
|
112 |
if (c.local_ip != IPAddress(0, 0, 0, 0)) {
|
|
|
113 |
item.setString("ip", c.local_ip.toString());
|
|
|
114 |
item.setString("gateway", c.gateway.toString());
|
|
|
115 |
item.setString("subnet", c.subnet.toString());
|
|
|
116 |
|
|
|
117 |
// Optional per-SSID DNS configuration
|
|
|
118 |
if (c.dns1 != IPAddress(0, 0, 0, 0)) {
|
|
|
119 |
item.setString("dns1", c.dns1.toString());
|
|
|
120 |
}
|
|
|
121 |
if (c.dns2 != IPAddress(0, 0, 0, 0)) {
|
|
|
122 |
item.setString("dns2", c.dns2.toString());
|
|
|
123 |
}
|
|
|
124 |
}
|
|
|
125 |
|
|
|
126 |
item.setNumber("hasPassword", c.pwd_len > 0 ? 1 : 0);
|
|
|
127 |
json_array.add(item);
|
|
|
128 |
}
|
|
|
129 |
}
|
|
|
130 |
}
|
|
|
131 |
request->send(200, "application/json", json_array.serialize());
|
|
|
132 |
});
|
|
|
133 |
// Delete a single credential (by index) or clear all if no index is provided
|
|
|
134 |
on("/wifi/credentials", HTTP_DELETE, [this](AsyncWebServerRequest *request) {
|
|
|
135 |
if (!m_credentialManager) {
|
|
|
136 |
request->send(404, "text/plain", "Credential manager not available");
|
|
|
137 |
return;
|
|
|
138 |
}
|
|
|
139 |
bool ok = false;
|
|
|
140 |
if (request->hasArg("index")) {
|
|
|
141 |
int idx = request->arg("index").toInt();
|
|
|
142 |
ok = m_credentialManager->removeCredential(static_cast<uint8_t>(idx));
|
|
|
143 |
} else {
|
|
|
144 |
m_credentialManager->clearAll();
|
|
|
145 |
ok = true;
|
|
|
146 |
}
|
|
|
147 |
#if defined(ESP32)
|
|
|
148 |
m_credentialManager->saveToNVS();
|
|
|
149 |
#elif defined(ESP8266)
|
|
|
150 |
m_credentialManager->saveToFS();
|
|
|
151 |
#endif
|
|
|
152 |
request->send(ok ? 200 : 400, "text/plain", ok ? "OK" : "Invalid index");
|
|
|
153 |
});
|
|
|
154 |
#endif
|
|
|
155 |
|
|
|
156 |
onNotFound([this](AsyncWebServerRequest *request) { this->notFound(request); });
|
|
|
157 |
serveStatic("/", *m_filesystem, "/").setDefaultFile("index.htm");
|
|
|
158 |
|
|
|
159 |
if (wsHandle != nullptr) {
|
|
|
160 |
if (!m_ws) m_ws = new AsyncWebSocket("/ws");
|
|
|
161 |
m_ws->onEvent(wsHandle);
|
|
|
162 |
addHandler(m_ws);
|
|
|
163 |
}
|
|
|
164 |
|
|
|
165 |
DefaultHeaders::Instance().addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
|
166 |
DefaultHeaders::Instance().addHeader("Pragma", "no-cache");
|
|
|
167 |
DefaultHeaders::Instance().addHeader("Expires", "0");
|
|
|
168 |
begin();
|
|
|
169 |
|
|
|
170 |
// MDNS is started only in AP mode (see startCaptivePortal)
|
|
|
171 |
return true;
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
void AsyncFsWebServer::printFileList(fs::FS &fs, const char * dirname, uint8_t levels) {
|
|
|
175 |
printFileList(fs, dirname, levels, Serial);
|
|
|
176 |
}
|
|
|
177 |
|
|
|
178 |
void AsyncFsWebServer::printFileList(fs::FS &fs, const char * dirname, uint8_t levels, Print& out) {
|
|
|
179 |
out.print("\nListing directory: ");
|
|
|
180 |
out.println(dirname);
|
|
|
181 |
File root = fs.open(dirname, "r");
|
|
|
182 |
if (!root) {
|
|
|
183 |
out.println("- failed to open directory");
|
|
|
184 |
return;
|
|
|
185 |
}
|
|
|
186 |
if (!root.isDirectory()) {
|
|
|
187 |
out.println(" - not a directory");
|
|
|
188 |
return;
|
|
|
189 |
}
|
|
|
190 |
File file = root.openNextFile();
|
|
|
191 |
while (file) {
|
|
|
192 |
if (file.isDirectory()) {
|
|
|
193 |
if (levels) {
|
|
|
194 |
#ifdef ESP32
|
|
|
195 |
printFileList(fs, file.path(), levels - 1, out);
|
|
|
196 |
#elif defined(ESP8266)
|
|
|
197 |
printFileList(fs, file.fullName(), levels - 1, out);
|
|
|
198 |
#endif
|
|
|
199 |
}
|
|
|
200 |
} else {
|
|
|
201 |
String line = "|__ FILE: ";
|
|
|
202 |
if (typeName == "SPIFFS") {
|
|
|
203 |
#ifdef ESP32
|
|
|
204 |
line += file.path();
|
|
|
205 |
#elif defined(ESP8266)
|
|
|
206 |
line += file.fullName();
|
|
|
207 |
#endif
|
|
|
208 |
} else {
|
|
|
209 |
line += file.name();
|
|
|
210 |
}
|
|
|
211 |
line += " (";
|
|
|
212 |
line += (unsigned long)file.size();
|
|
|
213 |
line += " bytes)";
|
|
|
214 |
out.println(line);
|
|
|
215 |
}
|
|
|
216 |
file = root.openNextFile();
|
|
|
217 |
}
|
|
|
218 |
}
|
|
|
219 |
|
|
|
220 |
#if ESP_FS_WS_EDIT
|
|
|
221 |
void AsyncFsWebServer::enableFsCodeEditor() {
|
|
|
222 |
on("/status", HTTP_GET, (ArRequestHandlerFunction)[this](AsyncWebServerRequest *request) { handleFsStatus(request); });
|
|
|
223 |
on("/list", HTTP_GET, (ArRequestHandlerFunction)[this](AsyncWebServerRequest *request) { handleFileList(request); });
|
|
|
224 |
on("/edit", HTTP_PUT, (ArRequestHandlerFunction)[this](AsyncWebServerRequest *request) { handleFileCreate(request); });
|
|
|
225 |
on("/edit", HTTP_DELETE, (ArRequestHandlerFunction)[this](AsyncWebServerRequest *request) { handleFileDelete(request); });
|
|
|
226 |
on("/edit", HTTP_GET, (ArRequestHandlerFunction)[this](AsyncWebServerRequest *request) { handleFileEdit(request); });
|
|
|
227 |
on("/edit", HTTP_POST,
|
|
|
228 |
[this](AsyncWebServerRequest *request) { sendOK(request); },
|
|
|
229 |
[this](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { handleUpload(request, filename, index, data, len, final); }
|
|
|
230 |
);
|
|
|
231 |
}
|
|
|
232 |
#endif
|
|
|
233 |
|
|
|
234 |
|
|
|
235 |
// Enable WebSocket handler at runtime. Creates WS on `path` and registers default handler.
|
|
|
236 |
void AsyncFsWebServer::enableWebSocket(const char* path, AwsEventHandler handler) {
|
|
|
237 |
if (m_ws) return;
|
|
|
238 |
m_ws = new AsyncWebSocket(path);
|
|
|
239 |
if (handler) {
|
|
|
240 |
m_ws->onEvent(handler);
|
|
|
241 |
}
|
|
|
242 |
addHandler(m_ws);
|
|
|
243 |
}
|
|
|
244 |
|
|
|
245 |
|
|
|
246 |
void AsyncFsWebServer::setAuthentication(const char* user, const char* pswd) {
|
|
|
247 |
// Free previous allocations if they exist
|
|
|
248 |
if (m_pageUser) {
|
|
|
249 |
free(m_pageUser);
|
|
|
250 |
m_pageUser = nullptr;
|
|
|
251 |
}
|
|
|
252 |
if (m_pagePswd) {
|
|
|
253 |
free(m_pagePswd);
|
|
|
254 |
m_pagePswd = nullptr;
|
|
|
255 |
}
|
|
|
256 |
|
|
|
257 |
// Validate input parameters
|
|
|
258 |
if (!user || !pswd || strlen(user) == 0 || strlen(pswd) == 0) {
|
|
|
259 |
log_error("Invalid authentication credentials");
|
|
|
260 |
return;
|
|
|
261 |
}
|
|
|
262 |
|
|
|
263 |
// Allocate with proper size (+1 for null terminator)
|
|
|
264 |
size_t userLen = strlen(user) + 1;
|
|
|
265 |
size_t pswnLen = strlen(pswd) + 1;
|
|
|
266 |
|
|
|
267 |
m_pageUser = (char*) malloc(userLen);
|
|
|
268 |
m_pagePswd = (char*) malloc(pswnLen);
|
|
|
269 |
|
|
|
270 |
if (m_pageUser && m_pagePswd) {
|
|
|
271 |
strncpy(m_pageUser, user, userLen - 1);
|
|
|
272 |
strncpy(m_pagePswd, pswd, pswnLen - 1);
|
|
|
273 |
m_pageUser[userLen - 1] = '\0';
|
|
|
274 |
m_pagePswd[pswnLen - 1] = '\0';
|
|
|
275 |
log_debug("Authentication credentials set successfully");
|
|
|
276 |
}
|
|
|
277 |
else {
|
|
|
278 |
log_error("Failed to allocate memory for authentication credentials");
|
|
|
279 |
if (m_pageUser) {
|
|
|
280 |
free(m_pageUser);
|
|
|
281 |
m_pageUser = nullptr;
|
|
|
282 |
}
|
|
|
283 |
if (m_pagePswd) {
|
|
|
284 |
free(m_pagePswd);
|
|
|
285 |
m_pagePswd = nullptr;
|
|
|
286 |
}
|
|
|
287 |
}
|
|
|
288 |
}
|
|
|
289 |
|
|
|
290 |
void AsyncFsWebServer::handleFileName(AsyncWebServerRequest *request) {
|
|
|
291 |
if (m_filesystem->exists(request->url()))
|
|
|
292 |
request->send(301, "text/plain", "OK");
|
|
|
293 |
request->send(404, "text/plain", "File not found");
|
|
|
294 |
}
|
|
|
295 |
|
|
|
296 |
void AsyncFsWebServer::sendOK(AsyncWebServerRequest *request) {
|
|
|
297 |
request->send(200, "text/plain", "OK");
|
|
|
298 |
}
|
|
|
299 |
|
|
|
300 |
void AsyncFsWebServer::notFound(AsyncWebServerRequest *request) {
|
|
|
301 |
|
|
|
302 |
// Check if authentication for all routes is turned on, and credentials are present:
|
|
|
303 |
if (m_authAll && m_pageUser != nullptr) {
|
|
|
304 |
if(!request->authenticate(m_pageUser, m_pagePswd))
|
|
|
305 |
return request->requestAuthentication();
|
|
|
306 |
}
|
|
|
307 |
|
|
|
308 |
String _url = request->url();
|
|
|
309 |
|
|
|
310 |
// Requested file not found, check if gzipped version exists
|
|
|
311 |
_url += ".gz";
|
|
|
312 |
if (!m_filesystem->exists(_url)) {
|
|
|
313 |
log_debug("File %s not found, checking for index redirection", request->url().c_str());
|
|
|
314 |
|
|
|
315 |
// File not found
|
|
|
316 |
if (request->url() == "/" && !m_filesystem->exists("/index.htm") && !m_filesystem->exists("/index.html")) {
|
|
|
317 |
request->redirect("/setup");
|
|
|
318 |
log_debug("Redirecting \"/\" to \"/setup\" (no index file found)");
|
|
|
319 |
return;
|
|
|
320 |
}
|
|
|
321 |
}
|
|
|
322 |
else {
|
|
|
323 |
log_debug("Serving gzipped file for %s", request->url().c_str());
|
|
|
324 |
request->redirect(_url);
|
|
|
325 |
return;
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
request->send(404, "text/plain", "AsyncFsWebServer: resource not found");
|
|
|
329 |
log_debug("Resource %s not found", request->url().c_str());
|
|
|
330 |
}
|
|
|
331 |
|
|
|
332 |
|
|
|
333 |
|
|
|
334 |
#if ESP_FS_WS_SETUP_HTM
|
|
|
335 |
void AsyncFsWebServer::handleSetup(AsyncWebServerRequest *request) {
|
|
|
336 |
if (m_pageUser != nullptr) {
|
|
|
337 |
if(!request->authenticate(m_pageUser, m_pagePswd))
|
|
|
338 |
return request->requestAuthentication();
|
|
|
339 |
}
|
|
|
340 |
|
|
|
341 |
// Changed array name to match SEGGER Bin2C output
|
|
|
342 |
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (uint8_t*)_acsetup_min_htm, sizeof(_acsetup_min_htm));
|
|
|
343 |
response->addHeader("Content-Encoding", "gzip");
|
|
|
344 |
response->addHeader("X-Config-File", ESP_FS_WS_CONFIG_FILE);
|
|
|
345 |
request->send(response);
|
|
|
346 |
}
|
|
|
347 |
#endif
|
|
|
348 |
|
|
|
349 |
#if ESP_FS_WS_SETUP
|
|
|
350 |
void AsyncFsWebServer::getStatus(AsyncWebServerRequest *request) {
|
|
|
351 |
CJSON::Json doc;
|
|
|
352 |
doc.setString("firmware", m_version);
|
|
|
353 |
String mode;
|
|
|
354 |
if (WiFi.status() == WL_CONNECTED) {
|
|
|
355 |
mode = "Station (";
|
|
|
356 |
mode += WiFi.SSID();
|
|
|
357 |
mode += ")";
|
|
|
358 |
} else {
|
|
|
359 |
mode = "Access Point";
|
|
|
360 |
}
|
|
|
361 |
doc.setString("mode", mode);
|
|
|
362 |
String ip = (WiFi.status() == WL_CONNECTED) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
|
|
363 |
doc.setString("ip", ip);
|
|
|
364 |
doc.setString("hostname", m_host);
|
|
|
365 |
doc.setString("path", String(ESP_FS_WS_CONFIG_FILE).substring(1)); // remove first '/'
|
|
|
366 |
doc.setString("liburl", LIB_URL);
|
|
|
367 |
|
|
|
368 |
// Add logo and page title for immediate availability (avoid layout shift during load)
|
|
|
369 |
String logoPath = "";
|
|
|
370 |
String pageTitle = "";
|
|
|
371 |
if (getSetupConfigurator()->getOptionValue("img-logo", logoPath) && logoPath.length() > 0) {
|
|
|
372 |
doc.setString("img-logo", logoPath);
|
|
|
373 |
}
|
|
|
374 |
if (getSetupConfigurator()->getOptionValue("page-title", pageTitle) && pageTitle.length() > 0) {
|
|
|
375 |
doc.setString("page-title", pageTitle);
|
|
|
376 |
}
|
|
|
377 |
|
|
|
378 |
String reply = doc.serialize();
|
|
|
379 |
request->send(200, "application/json", reply);
|
|
|
380 |
}
|
|
|
381 |
|
|
|
382 |
void AsyncFsWebServer::clearConfig(AsyncWebServerRequest *request) {
|
|
|
383 |
if (m_filesystem->remove(ESP_FS_WS_CONFIG_FILE))
|
|
|
384 |
request->send(200, "text/plain", "Clear config OK");
|
|
|
385 |
else
|
|
|
386 |
request->send(200, "text/plain", "Clear config not done");
|
|
|
387 |
}
|
|
|
388 |
|
|
|
389 |
|
|
|
390 |
void AsyncFsWebServer::handleScanNetworks(AsyncWebServerRequest *request) {
|
|
|
391 |
log_info("Start scan WiFi networks");
|
|
|
392 |
WiFiScanResult scan = WiFiService::scanNetworks();
|
|
|
393 |
request->send(200, "application/json", scan.json);
|
|
|
394 |
}
|
|
|
395 |
|
|
|
396 |
|
|
|
397 |
bool AsyncFsWebServer::createDirFromPath(const String& path) {
|
|
|
398 |
String dir;
|
|
|
399 |
dir.reserve(path.length());
|
|
|
400 |
int p1 = 0; int p2 = 0;
|
|
|
401 |
while (p2 != -1) {
|
|
|
402 |
p2 = path.indexOf("/", p1 + 1);
|
|
|
403 |
dir += path.substring(p1, p2);
|
|
|
404 |
// Check if its a valid dir
|
|
|
405 |
if (dir.indexOf(".") == -1) {
|
|
|
406 |
if (!m_filesystem->exists(dir)) {
|
|
|
407 |
if (m_filesystem->mkdir(dir)) {
|
|
|
408 |
log_info("Folder %s created\n", dir.c_str());
|
|
|
409 |
} else {
|
|
|
410 |
log_info("Error. Folder %s not created\n", dir.c_str());
|
|
|
411 |
return false;
|
|
|
412 |
}
|
|
|
413 |
}
|
|
|
414 |
}
|
|
|
415 |
p1 = p2;
|
|
|
416 |
}
|
|
|
417 |
return true;
|
|
|
418 |
}
|
|
|
419 |
|
|
|
420 |
void AsyncFsWebServer::handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
|
421 |
|
|
|
422 |
// DebugPrintln("Handle upload POST");
|
|
|
423 |
if (!index) {
|
|
|
424 |
// Increase task WDT timeout
|
|
|
425 |
setTaskWdt(AWS_LONG_WDT_TIMEOUT);
|
|
|
426 |
|
|
|
427 |
// Create folder if necessary (up to max 5 sublevels)
|
|
|
428 |
size_t filenameLen = filename.length();
|
|
|
429 |
|
|
|
430 |
// Protect against excessively long filenames
|
|
|
431 |
if (filenameLen >= 512) {
|
|
|
432 |
log_error("Filename too long (max 512 bytes): %s", filename.c_str());
|
|
|
433 |
request->_tempFile.close();
|
|
|
434 |
return;
|
|
|
435 |
}
|
|
|
436 |
|
|
|
437 |
char path[filenameLen + 1];
|
|
|
438 |
strcpy(path, filename.c_str());
|
|
|
439 |
createDirFromPath(path);
|
|
|
440 |
|
|
|
441 |
// open the file on first call and store the file handle in the request object
|
|
|
442 |
request->_tempFile = m_filesystem->open(filename, "w");
|
|
|
443 |
log_debug("Upload Start: writing file %s", filename.c_str());
|
|
|
444 |
}
|
|
|
445 |
|
|
|
446 |
if (len) {
|
|
|
447 |
// stream the incoming chunk to the opened file
|
|
|
448 |
request->_tempFile.write(data, len);
|
|
|
449 |
}
|
|
|
450 |
|
|
|
451 |
if (final) {
|
|
|
452 |
// Restore task WDT timeout
|
|
|
453 |
setTaskWdt(AWS_WDT_TIMEOUT);
|
|
|
454 |
// close the file handle as the upload is now done
|
|
|
455 |
request->_tempFile.close();
|
|
|
456 |
log_debug("Upload complete: %s, size: %zu (index: %zu)", filename.c_str(), index + len, index);
|
|
|
457 |
|
|
|
458 |
// Call config saved callback if this is the config file
|
|
|
459 |
if (filename == ESP_FS_WS_CONFIG_FILE && m_configSavedCallback) {
|
|
|
460 |
log_debug("Config file saved, calling callback");
|
|
|
461 |
m_configSavedCallback(filename.c_str());
|
|
|
462 |
}
|
|
|
463 |
}
|
|
|
464 |
}
|
|
|
465 |
|
|
|
466 |
void AsyncFsWebServer::doWifiConnection(AsyncWebServerRequest *request) {
|
|
|
467 |
// Use WiFiConnectParams as the main container for all connection options
|
|
|
468 |
WiFiConnectParams params = WiFiConnectParams();
|
|
|
469 |
String requestedHost; // Optional hostname provided by the setup UI
|
|
|
470 |
|
|
|
471 |
// Optional static IP configuration
|
|
|
472 |
if (request->hasArg("ip_address") && request->hasArg("subnet") && request->hasArg("gateway")) {
|
|
|
473 |
params.creds.gateway.fromString(request->arg("gateway"));
|
|
|
474 |
params.creds.subnet.fromString(request->arg("subnet"));
|
|
|
475 |
params.creds.local_ip.fromString(request->arg("ip_address"));
|
|
|
476 |
params.dhcp = false;
|
|
|
477 |
log_debug("Static IP requested: %s, GW: %s, SN: %s",
|
|
|
478 |
params.creds.local_ip.toString().c_str(),
|
|
|
479 |
params.creds.gateway.toString().c_str(),
|
|
|
480 |
params.creds.subnet.toString().c_str());
|
|
|
481 |
log_debug("params.dhcp set to: %d", params.dhcp);
|
|
|
482 |
}
|
|
|
483 |
|
|
|
484 |
// Optional per-SSID DNS configuration (if provided by client)
|
|
|
485 |
if (request->hasArg("dns1"))
|
|
|
486 |
params.creds.dns1.fromString(request->arg("dns1"));
|
|
|
487 |
|
|
|
488 |
if (request->hasArg("dns2"))
|
|
|
489 |
params.creds.dns2.fromString(request->arg("dns2"));
|
|
|
490 |
|
|
|
491 |
// Optional hostname override (for mDNS / shared hostname, max 32 chars)
|
|
|
492 |
if (request->hasArg("hostname")) {
|
|
|
493 |
requestedHost = request->arg("hostname");
|
|
|
494 |
requestedHost.trim();
|
|
|
495 |
if (requestedHost.length() > 32) {
|
|
|
496 |
requestedHost.remove(32); // limit to 32 chars
|
|
|
497 |
}
|
|
|
498 |
if (requestedHost.length()) {
|
|
|
499 |
m_host = requestedHost;
|
|
|
500 |
if (m_credentialManager) {
|
|
|
501 |
m_credentialManager->setHostname(m_host.c_str());
|
|
|
502 |
}
|
|
|
503 |
}
|
|
|
504 |
}
|
|
|
505 |
|
|
|
506 |
// Basic WiFi credentials
|
|
|
507 |
if (request->hasArg("ssid")) {
|
|
|
508 |
String ssid = request->arg("ssid");
|
|
|
509 |
strncpy(params.creds.ssid, ssid.c_str(), sizeof(params.creds.ssid) - 1);
|
|
|
510 |
params.creds.ssid[sizeof(params.creds.ssid) - 1] = '\0';
|
|
|
511 |
}
|
|
|
512 |
if (request->hasArg("password"))
|
|
|
513 |
params.password = request->arg("password");
|
|
|
514 |
|
|
|
515 |
// If no password provided but a stored credential exists for this SSID,
|
|
|
516 |
// reuse the stored password without exposing it to the client.
|
|
|
517 |
if (params.password.length() == 0 && strlen(params.creds.ssid) && m_credentialManager &&
|
|
|
518 |
m_credentialManager->checkSSIDExists(params.creds.ssid)) {
|
|
|
519 |
String stored = m_credentialManager->getPassword(params.creds.ssid);
|
|
|
520 |
if (stored.length()) {
|
|
|
521 |
params.password = stored;
|
|
|
522 |
}
|
|
|
523 |
}
|
|
|
524 |
|
|
|
525 |
// Track if we had a working STA connection before this /connect
|
|
|
526 |
// (used to decide fallbacks on failure).
|
|
|
527 |
bool wasStaConnected = (WiFi.status() == WL_CONNECTED && WiFi.getMode() != WIFI_AP);
|
|
|
528 |
|
|
|
529 |
// By default, new credentials will be persisted to survive reboots,
|
|
|
530 |
// but the client can disable this if they want a temporary connection.
|
|
|
531 |
bool hasPersistentArg = request->hasArg("persistent");
|
|
|
532 |
bool persistent = hasPersistentArg ? request->arg("persistent").equals("true") : true;
|
|
|
533 |
WiFi.persistent(hasPersistentArg ? persistent : true); // default true
|
|
|
534 |
|
|
|
535 |
// Remember if this /connect was requested while we are serving the captive portal (AP mode).
|
|
|
536 |
params.fromApClient = m_isApMode;
|
|
|
537 |
params.host = m_host;
|
|
|
538 |
params.timeout = m_timeout;
|
|
|
539 |
params.wdtLongTimeout = AWS_LONG_WDT_TIMEOUT;
|
|
|
540 |
params.wdtTimeout = AWS_WDT_TIMEOUT;
|
|
|
541 |
|
|
|
542 |
// In AP/captive-portal mode we can still safely perform the
|
|
|
543 |
// connection synchronously and return the detailed HTML result, because the AP interface remains up.
|
|
|
544 |
if (params.fromApClient) {
|
|
|
545 |
WiFiConnectResult result = WiFiService::connectWithParams(params);
|
|
|
546 |
|
|
|
547 |
if (result.connected && persistent && strlen(params.creds.ssid) && params.password.length() && m_credentialManager) {
|
|
|
548 |
// Credential is already populated in params.credential, just need to configure it based on dhcp flag
|
|
|
549 |
if (params.dhcp) {
|
|
|
550 |
// Clear static IP config for DHCP mode
|
|
|
551 |
params.creds.local_ip = IPAddress(0, 0, 0, 0);
|
|
|
552 |
params.creds.gateway = IPAddress(0, 0, 0, 0);
|
|
|
553 |
params.creds.subnet = IPAddress(0, 0, 0, 0);
|
|
|
554 |
params.creds.dns1 = IPAddress(0, 0, 0, 0);
|
|
|
555 |
params.creds.dns2 = IPAddress(0, 0, 0, 0);
|
|
|
556 |
}
|
|
|
557 |
// Static IP config already set in params.creds if dhcp == false
|
|
|
558 |
if (!m_credentialManager->updateCredential(params.creds, params.password.c_str())) {
|
|
|
559 |
m_credentialManager->addCredential(params.creds, params.password.c_str());
|
|
|
560 |
}
|
|
|
561 |
#if defined(ESP32)
|
|
|
562 |
m_credentialManager->saveToNVS();
|
|
|
563 |
#elif defined(ESP8266)
|
|
|
564 |
m_credentialManager->saveToFS();
|
|
|
565 |
#endif
|
|
|
566 |
}
|
|
|
567 |
|
|
|
568 |
if (result.connected) {
|
|
|
569 |
m_serverIp = result.ip;
|
|
|
570 |
}
|
|
|
571 |
|
|
|
572 |
log_debug("WiFi connect result body: %s", result.body.c_str());
|
|
|
573 |
const char* contentType = (result.status == 200) ? "text/html" : "text/plain";
|
|
|
574 |
request->send(result.status, contentType, result.body);
|
|
|
575 |
return;
|
|
|
576 |
}
|
|
|
577 |
|
|
|
578 |
// STA mode: if already connected, always ask for confirmation before reconfiguring
|
|
|
579 |
bool userConfirmed = request->hasArg("confirmed") && request->arg("confirmed").equals("true");
|
|
|
580 |
|
|
|
581 |
if (!userConfirmed && WiFi.status() == WL_CONNECTED) {
|
|
|
582 |
String resp;
|
|
|
583 |
resp.reserve(512);
|
|
|
584 |
resp = "<div id='action-confirm-sta-change'></div>";
|
|
|
585 |
resp += "You are about to apply new WiFi configuration for <b>";
|
|
|
586 |
resp += String(params.creds.ssid);
|
|
|
587 |
resp += "</b>.<br><br><i>Note: This page may lose connection during the reconfiguration.</i>";
|
|
|
588 |
resp += "<br><br>Do you want to proceed?";
|
|
|
589 |
request->send(200, "text/html", resp);
|
|
|
590 |
return;
|
|
|
591 |
}
|
|
|
592 |
|
|
|
593 |
// User confirmed: proceed with connection,
|
|
|
594 |
// but first set up the disconnect handler to attempt connection after response is sent (to avoid HTTP timeouts)
|
|
|
595 |
|
|
|
596 |
// Attempt connection AFTER sending response
|
|
|
597 |
request->onDisconnect([this, request, params, wasStaConnected]() mutable {
|
|
|
598 |
WiFiConnectResult result = WiFiService::connectWithParams(params);
|
|
|
599 |
log_debug("Connection result: connected=%d", result.connected);
|
|
|
600 |
|
|
|
601 |
// Save credentials ONLY if connection succeeded
|
|
|
602 |
if (result.connected && strlen(params.creds.ssid) && params.password.length() && m_credentialManager) {
|
|
|
603 |
if (params.dhcp) {
|
|
|
604 |
// Clear static IP config for DHCP mode
|
|
|
605 |
params.creds.local_ip = IPAddress(0, 0, 0, 0);
|
|
|
606 |
params.creds.gateway = IPAddress(0, 0, 0, 0);
|
|
|
607 |
params.creds.subnet = IPAddress(0, 0, 0, 0);
|
|
|
608 |
params.creds.dns1 = IPAddress(0, 0, 0, 0);
|
|
|
609 |
params.creds.dns2 = IPAddress(0, 0, 0, 0);
|
|
|
610 |
log_debug("Saving DHCP config");
|
|
|
611 |
} else {
|
|
|
612 |
log_debug("Saving static IP: IP=%s, GW=%s, SN=%s, DNS1=%s, DNS2=%s",
|
|
|
613 |
params.creds.local_ip.toString().c_str(),
|
|
|
614 |
params.creds.gateway.toString().c_str(),
|
|
|
615 |
params.creds.subnet.toString().c_str(),
|
|
|
616 |
params.creds.dns1.toString().c_str(),
|
|
|
617 |
params.creds.dns2.toString().c_str());
|
|
|
618 |
}
|
|
|
619 |
|
|
|
620 |
if (!m_credentialManager->updateCredential(params.creds, params.password.c_str())) {
|
|
|
621 |
m_credentialManager->addCredential(params.creds, params.password.c_str());
|
|
|
622 |
}
|
|
|
623 |
#if defined(ESP32)
|
|
|
624 |
m_credentialManager->saveToNVS();
|
|
|
625 |
#elif defined(ESP8266)
|
|
|
626 |
m_credentialManager->saveToFS();
|
|
|
627 |
#endif
|
|
|
628 |
}
|
|
|
629 |
|
|
|
630 |
if (result.connected) {
|
|
|
631 |
m_serverIp = result.ip;
|
|
|
632 |
}
|
|
|
633 |
else if (wasStaConnected && !params.fromApClient && strlen(m_apSSID) > 0) {
|
|
|
634 |
log_info("WiFi connect failed, starting AP mode: SSID=%s", m_apSSID);
|
|
|
635 |
startCaptivePortal(m_apSSID, m_apPassword, "/setup");
|
|
|
636 |
}
|
|
|
637 |
});
|
|
|
638 |
|
|
|
639 |
// Send response FIRST to avoid HTTP timeout
|
|
|
640 |
String resp;
|
|
|
641 |
resp.reserve(512);
|
|
|
642 |
resp = "<div id='action-async-applying'></div>";
|
|
|
643 |
resp += "WiFi configuration applying for <b>";
|
|
|
644 |
resp += String(params.creds.ssid);
|
|
|
645 |
resp += "</b>.<br><br>";
|
|
|
646 |
resp += "<i>The ESP is reconnecting...<br>Please reload this page after a few seconds.</i>";
|
|
|
647 |
request->send(200, "text/html", resp);
|
|
|
648 |
}
|
|
|
649 |
|
|
|
650 |
void AsyncFsWebServer::onUpdate() {
|
|
|
651 |
#if defined(ESP8266)
|
|
|
652 |
on("/update", HTTP_POST, [](AsyncWebServerRequest *request){
|
|
|
653 |
// the request handler is triggered after the upload has finished...
|
|
|
654 |
// create the response, add header, and send response
|
|
|
655 |
String txt = Update.hasError() ? Update.getErrorString() : "Update Success<br>Restart ESP to load new firmware!\n";
|
|
|
656 |
AsyncWebServerResponse *response = request->beginResponse((Update.hasError()) ? 500 : 200, "text/plain", txt);
|
|
|
657 |
response->addHeader("Connection", "close");
|
|
|
658 |
response->addHeader("Access-Control-Allow-Origin", "*");
|
|
|
659 |
request->send(response);
|
|
|
660 |
},
|
|
|
661 |
|
|
|
662 |
//Upload handler chunks in data
|
|
|
663 |
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){
|
|
|
664 |
// if index == 0 then this is the first frame of data
|
|
|
665 |
if(!index){
|
|
|
666 |
DBG_OUTPUT_PORT.printf("UploadStart: %s\n", filename.c_str());
|
|
|
667 |
DBG_OUTPUT_PORT.setDebugOutput(true);
|
|
|
668 |
|
|
|
669 |
// calculate sketch space required for the update
|
|
|
670 |
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
|
|
671 |
if(!Update.begin(maxSketchSpace)){//start with max available size
|
|
|
672 |
Update.printError(DBG_OUTPUT_PORT);
|
|
|
673 |
return request->send(500, "text/plain", "OTA failed to start");
|
|
|
674 |
}
|
|
|
675 |
Update.runAsync(true); // tell the updaterClass to run in async mode
|
|
|
676 |
}
|
|
|
677 |
|
|
|
678 |
//Write chunked data to the free sketch space
|
|
|
679 |
if(Update.write(data, len) != len){
|
|
|
680 |
Update.printError(DBG_OUTPUT_PORT);
|
|
|
681 |
return request->send(500, "text/plain", "OTA failed to write chunk");
|
|
|
682 |
}
|
|
|
683 |
|
|
|
684 |
// if the final flag is set then this is the last frame of data
|
|
|
685 |
if(final){
|
|
|
686 |
if(Update.end(true)){ //true to set the size to the current progress
|
|
|
687 |
DBG_OUTPUT_PORT.printf("Update Success: %u bytes written.\nRestart ESP!\n", index + len);
|
|
|
688 |
}
|
|
|
689 |
else {
|
|
|
690 |
Update.printError(DBG_OUTPUT_PORT);
|
|
|
691 |
return request->send(500, "text/plain", "OTA failed");
|
|
|
692 |
}
|
|
|
693 |
DBG_OUTPUT_PORT.setDebugOutput(false);
|
|
|
694 |
}
|
|
|
695 |
});
|
|
|
696 |
|
|
|
697 |
#elif defined(ESP32)
|
|
|
698 |
on("/update", HTTP_POST, [](AsyncWebServerRequest *request){
|
|
|
699 |
// the request handler is triggered after the upload has finished...
|
|
|
700 |
// create the response, add header, and send response
|
|
|
701 |
|
|
|
702 |
String txt = Update.hasError() ? Update.errorString() : "Success! Restart ESP to load new firmware!\n";
|
|
|
703 |
AsyncWebServerResponse *response = request->beginResponse((Update.hasError()) ? 500 : 200, "text/plain", txt);
|
|
|
704 |
response->addHeader("Connection", "close");
|
|
|
705 |
response->addHeader("Access-Control-Allow-Origin", "*");
|
|
|
706 |
request->send(response);
|
|
|
707 |
},
|
|
|
708 |
|
|
|
709 |
//Upload handler chunks in data
|
|
|
710 |
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){
|
|
|
711 |
|
|
|
712 |
if (!index) {
|
|
|
713 |
log_info("Firmware size: %d", request->getHeader("Content-Length")->value().length());
|
|
|
714 |
|
|
|
715 |
// Increase task WDT timeout
|
|
|
716 |
setTaskWdt(AWS_LONG_WDT_TIMEOUT);
|
|
|
717 |
if (!Update.begin()) {
|
|
|
718 |
log_error("Update begin failed");
|
|
|
719 |
Update.printError(DBG_OUTPUT_PORT);
|
|
|
720 |
return request->send(500, "text/plain", "OTA could not begin");
|
|
|
721 |
}
|
|
|
722 |
}
|
|
|
723 |
|
|
|
724 |
// Write chunked data to the free sketch space
|
|
|
725 |
if (len){
|
|
|
726 |
esp_task_wdt_reset();
|
|
|
727 |
if (Update.write(data, len) != len) {
|
|
|
728 |
log_error("Update write failed");
|
|
|
729 |
return request->send(500, "text/plain", "OTA write failed");
|
|
|
730 |
}
|
|
|
731 |
}
|
|
|
732 |
|
|
|
733 |
// if the final flag is set then this is the last frame of data
|
|
|
734 |
if (final) {
|
|
|
735 |
log_info("Update End.");
|
|
|
736 |
if (!Update.end(true)) { //true to set the size to the current progress
|
|
|
737 |
log_error("%s\n", Update.errorString());
|
|
|
738 |
setTaskWdt(AWS_WDT_TIMEOUT);
|
|
|
739 |
return request->send(500, "text/plain", "Could not end OTA");
|
|
|
740 |
}
|
|
|
741 |
log_info("Update Success.\nRestart ESP!\n");
|
|
|
742 |
setTaskWdt(AWS_WDT_TIMEOUT);
|
|
|
743 |
}
|
|
|
744 |
});
|
|
|
745 |
#endif
|
|
|
746 |
}
|
|
|
747 |
|
|
|
748 |
#endif //ESP_FS_WS_SETUP
|
|
|
749 |
|
|
|
750 |
bool AsyncFsWebServer::startMDNSResponder() {
|
|
|
751 |
#if ESP_FS_WS_MDNS
|
|
|
752 |
return WiFiService::startMDNSResponder(m_dnsServer, m_host, m_port, m_serverIp);
|
|
|
753 |
#else
|
|
|
754 |
return true;
|
|
|
755 |
#endif
|
|
|
756 |
}
|
|
|
757 |
|
|
|
758 |
bool AsyncFsWebServer::startWiFi(uint32_t timeout, CallbackF fn) {
|
|
|
759 |
#ifdef ESP32
|
|
|
760 |
if (m_credentialManager && m_credentialManager->loadFromNVS()) {
|
|
|
761 |
log_debug("Credentials loaded from NVS");
|
|
|
762 |
log_debug("%s", m_credentialManager->getDebugInfo().c_str());
|
|
|
763 |
}
|
|
|
764 |
#else
|
|
|
765 |
if (m_credentialManager && m_credentialManager->loadFromFS()) {
|
|
|
766 |
log_debug("Credentials loaded from filesystem");
|
|
|
767 |
log_debug("%s", m_credentialManager->getDebugInfo().c_str());
|
|
|
768 |
}
|
|
|
769 |
#endif
|
|
|
770 |
|
|
|
771 |
// If a shared hostname was stored in CredentialManager, prefer it
|
|
|
772 |
if (m_credentialManager) {
|
|
|
773 |
String storedHost = m_credentialManager->getHostname();
|
|
|
774 |
if (storedHost.length()) {
|
|
|
775 |
m_host = storedHost;
|
|
|
776 |
}
|
|
|
777 |
}
|
|
|
778 |
|
|
|
779 |
WiFiStartResult result = WiFiService::startWiFi(m_credentialManager, m_filesystem, ESP_FS_WS_CONFIG_FILE, timeout);
|
|
|
780 |
if (result.action == WiFiStartAction::Connected) {
|
|
|
781 |
m_serverIp = result.ip;
|
|
|
782 |
m_isApMode = false;
|
|
|
783 |
#if ESP_FS_WS_MDNS
|
|
|
784 |
WiFiService::startMDNSOnly(m_host, m_port);
|
|
|
785 |
log_info("mDNS started on http://%s.local", m_host.c_str());
|
|
|
786 |
#endif
|
|
|
787 |
return true;
|
|
|
788 |
}
|
|
|
789 |
|
|
|
790 |
if (result.action == WiFiStartAction::StartAp && strlen(m_apSSID) > 0) {
|
|
|
791 |
log_info("Starting AP mode: SSID=%s", m_apSSID);
|
|
|
792 |
startCaptivePortal(m_apSSID, m_apPassword, "/setup");
|
|
|
793 |
return true;
|
|
|
794 |
}
|
|
|
795 |
return false;
|
|
|
796 |
}
|
|
|
797 |
|
|
|
798 |
|
|
|
799 |
bool AsyncFsWebServer::startCaptivePortal(const char* ssid, const char* pass, const char* redirectTargetURL) {
|
|
|
800 |
WiFiConnectParams params;
|
|
|
801 |
strncpy(params.creds.ssid, ssid, sizeof(params.creds.ssid) - 1);
|
|
|
802 |
params.creds.ssid[sizeof(params.creds.ssid) - 1] = '\0';
|
|
|
803 |
params.password = pass;
|
|
|
804 |
return startCaptivePortal(params, redirectTargetURL);
|
|
|
805 |
}
|
|
|
806 |
|
|
|
807 |
|
|
|
808 |
bool AsyncFsWebServer::startCaptivePortal(WiFiConnectParams& params, const char *redirectTargetURL) {
|
|
|
809 |
// Start AP mode
|
|
|
810 |
if (!WiFiService::startAccessPoint(params, m_serverIp)) {
|
|
|
811 |
return false;
|
|
|
812 |
}
|
|
|
813 |
m_isApMode = true;
|
|
|
814 |
// Start DNS server
|
|
|
815 |
this->startMDNSResponder();
|
|
|
816 |
// Captive portal server to redirect all requests to the AP IP
|
|
|
817 |
m_captive = new CaptiveRequestHandler(redirectTargetURL);
|
|
|
818 |
addHandler(m_captive).setFilter(ON_AP_FILTER); //only when requested from AP
|
|
|
819 |
|
|
|
820 |
log_info("Captive portal started. Redirecting all requests to %s", redirectTargetURL);
|
|
|
821 |
return true;
|
|
|
822 |
}
|
|
|
823 |
|
|
|
824 |
// edit page, in usefull in some situation, but if you need to provide only a web interface, you can disable
|
|
|
825 |
#if ESP_FS_WS_EDIT
|
|
|
826 |
|
|
|
827 |
void AsyncFsWebServer::handleFileEdit(AsyncWebServerRequest *request) {
|
|
|
828 |
if (m_pageUser != nullptr) {
|
|
|
829 |
if(!request->authenticate(m_pageUser, m_pagePswd))
|
|
|
830 |
return request->requestAuthentication();
|
|
|
831 |
}
|
|
|
832 |
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", (uint8_t*)_acedit_htm, sizeof(_acedit_htm));
|
|
|
833 |
response->addHeader("Content-Encoding", "gzip");
|
|
|
834 |
request->send(response);
|
|
|
835 |
}
|
|
|
836 |
|
|
|
837 |
/*
|
|
|
838 |
Return the list of files in the directory specified by the "dir" query string parameter.
|
|
|
839 |
Also demonstrates the use of chunked responses.
|
|
|
840 |
*/
|
|
|
841 |
void AsyncFsWebServer::handleFileList(AsyncWebServerRequest *request)
|
|
|
842 |
{
|
|
|
843 |
if (!request->hasArg("dir")) {
|
|
|
844 |
return request->send(400, "DIR ARG MISSING");
|
|
|
845 |
}
|
|
|
846 |
|
|
|
847 |
String path = request->arg("dir");
|
|
|
848 |
log_debug("handleFileList: %s", path.c_str());
|
|
|
849 |
if (path != "/" && !m_filesystem->exists(path)) {
|
|
|
850 |
return request->send(400, "BAD PATH");
|
|
|
851 |
}
|
|
|
852 |
|
|
|
853 |
File root = m_filesystem->open(path, "r");
|
|
|
854 |
String output;
|
|
|
855 |
output.reserve(512);
|
|
|
856 |
output = "[";
|
|
|
857 |
if (root.isDirectory()) {
|
|
|
858 |
File file = root.openNextFile();
|
|
|
859 |
bool first = true;
|
|
|
860 |
while (file) {
|
|
|
861 |
if (!first) output += ",";
|
|
|
862 |
first = false;
|
|
|
863 |
String filename;
|
|
|
864 |
if (typeName.equals("SPIFFS")) {
|
|
|
865 |
// SPIFFS returns full path and subfolders are unsupported, remove leading '/'
|
|
|
866 |
#if defined(ESP32)
|
|
|
867 |
filename += file.path();
|
|
|
868 |
#elif defined(ESP8266)
|
|
|
869 |
filename += file.fullName();
|
|
|
870 |
#endif
|
|
|
871 |
filename.remove(0, 1);
|
|
|
872 |
}
|
|
|
873 |
else {
|
|
|
874 |
filename = file.name();
|
|
|
875 |
if (filename.lastIndexOf("/") > -1) {
|
|
|
876 |
filename.remove(0, filename.lastIndexOf("/") + 1);
|
|
|
877 |
}
|
|
|
878 |
}
|
|
|
879 |
CJSON::Json item;
|
|
|
880 |
item.setString("type", (file.isDirectory()) ? "dir" : "file");
|
|
|
881 |
item.setNumber("size", file.size());
|
|
|
882 |
item.setString("name", filename);
|
|
|
883 |
output += item.serialize();
|
|
|
884 |
file = root.openNextFile();
|
|
|
885 |
}
|
|
|
886 |
}
|
|
|
887 |
output += "]";
|
|
|
888 |
request->send(200, "text/json", output);
|
|
|
889 |
}
|
|
|
890 |
|
|
|
891 |
/*
|
|
|
892 |
Handle the creation/rename of a new file
|
|
|
893 |
Operation | req.responseText
|
|
|
894 |
---------------+--------------------------------------------------------------
|
|
|
895 |
Create file | parent of created file
|
|
|
896 |
Create folder | parent of created folder
|
|
|
897 |
Rename file | parent of source file
|
|
|
898 |
Move file | parent of source file, or remaining ancestor
|
|
|
899 |
Rename folder | parent of source folder
|
|
|
900 |
Move folder | parent of source folder, or remaining ancestor
|
|
|
901 |
*/
|
|
|
902 |
void AsyncFsWebServer::handleFileCreate(AsyncWebServerRequest *request)
|
|
|
903 |
{
|
|
|
904 |
String path = request->arg("path");
|
|
|
905 |
if (path.isEmpty()) {
|
|
|
906 |
return request->send(400, "PATH ARG MISSING");
|
|
|
907 |
}
|
|
|
908 |
if (path == "/") {
|
|
|
909 |
return request->send(400, "BAD PATH");
|
|
|
910 |
}
|
|
|
911 |
|
|
|
912 |
String src = request->arg("src");
|
|
|
913 |
if (src.isEmpty()) {
|
|
|
914 |
// No source specified: creation
|
|
|
915 |
log_debug("handleFileCreate: %s\n", path.c_str());
|
|
|
916 |
if (path.endsWith("/")) {
|
|
|
917 |
// Create a folder
|
|
|
918 |
path.remove(path.length() - 1);
|
|
|
919 |
if (!m_filesystem->mkdir(path)) {
|
|
|
920 |
return request->send(500, "MKDIR FAILED");
|
|
|
921 |
}
|
|
|
922 |
}
|
|
|
923 |
else {
|
|
|
924 |
// Create a file
|
|
|
925 |
File file = m_filesystem->open(path, "w");
|
|
|
926 |
if (file) {
|
|
|
927 |
file.write(0);
|
|
|
928 |
file.close();
|
|
|
929 |
}
|
|
|
930 |
else {
|
|
|
931 |
return request->send(500, "CREATE FAILED");
|
|
|
932 |
}
|
|
|
933 |
}
|
|
|
934 |
request->send(200, path.c_str());
|
|
|
935 |
}
|
|
|
936 |
else {
|
|
|
937 |
// Source specified: rename
|
|
|
938 |
if (src == "/") {
|
|
|
939 |
return request->send(400, "BAD SRC");
|
|
|
940 |
}
|
|
|
941 |
if (!m_filesystem->exists(src)) {
|
|
|
942 |
return request->send(400, "FILE NOT FOUND");
|
|
|
943 |
}
|
|
|
944 |
|
|
|
945 |
log_debug("handleFileCreate: %s from %s\n", path.c_str(), src.c_str());
|
|
|
946 |
if (path.endsWith("/")) {
|
|
|
947 |
path.remove(path.length() - 1);
|
|
|
948 |
}
|
|
|
949 |
if (src.endsWith("/")) {
|
|
|
950 |
src.remove(src.length() - 1);
|
|
|
951 |
}
|
|
|
952 |
if (!m_filesystem->rename(src, path)) {
|
|
|
953 |
return request->send(500, "RENAME FAILED");
|
|
|
954 |
}
|
|
|
955 |
sendOK(request);
|
|
|
956 |
}
|
|
|
957 |
}
|
|
|
958 |
|
|
|
959 |
/*
|
|
|
960 |
Handle a file deletion request
|
|
|
961 |
Operation | req.responseText
|
|
|
962 |
---------------+--------------------------------------------------------------
|
|
|
963 |
Delete file | parent of deleted file, or remaining ancestor
|
|
|
964 |
Delete folder | parent of deleted folder, or remaining ancestor
|
|
|
965 |
*/
|
|
|
966 |
|
|
|
967 |
void AsyncFsWebServer::deleteContent(String& path) {
|
|
|
968 |
File file = m_filesystem->open(path.c_str(), "r");
|
|
|
969 |
if (!file.isDirectory()) {
|
|
|
970 |
file.close();
|
|
|
971 |
m_filesystem->remove(path.c_str());
|
|
|
972 |
log_info("File %s deleted", path.c_str());
|
|
|
973 |
return;
|
|
|
974 |
}
|
|
|
975 |
|
|
|
976 |
file.rewindDirectory();
|
|
|
977 |
while (true) {
|
|
|
978 |
File entry = file.openNextFile();
|
|
|
979 |
if (!entry) {
|
|
|
980 |
break;
|
|
|
981 |
}
|
|
|
982 |
String entryPath = path;
|
|
|
983 |
entryPath += "/";
|
|
|
984 |
entryPath += entry.name();
|
|
|
985 |
if (entry.isDirectory()) {
|
|
|
986 |
entry.close();
|
|
|
987 |
deleteContent(entryPath);
|
|
|
988 |
}
|
|
|
989 |
else {
|
|
|
990 |
entry.close();
|
|
|
991 |
m_filesystem->remove(entryPath.c_str());
|
|
|
992 |
log_info("File %s deleted", path.c_str());
|
|
|
993 |
}
|
|
|
994 |
yield();
|
|
|
995 |
}
|
|
|
996 |
m_filesystem->rmdir(path.c_str());
|
|
|
997 |
log_info("Folder %s removed", path.c_str());
|
|
|
998 |
file.close();
|
|
|
999 |
}
|
|
|
1000 |
|
|
|
1001 |
|
|
|
1002 |
|
|
|
1003 |
void AsyncFsWebServer::handleFileDelete(AsyncWebServerRequest *request) {
|
|
|
1004 |
String path = request->arg((size_t)0);
|
|
|
1005 |
if (path.isEmpty() || path == "/") {
|
|
|
1006 |
return request->send(400, "BAD PATH");
|
|
|
1007 |
}
|
|
|
1008 |
if (!m_filesystem->exists(path)) {
|
|
|
1009 |
return request->send(400, "File Not Found");
|
|
|
1010 |
}
|
|
|
1011 |
deleteContent(path);
|
|
|
1012 |
sendOK(request);
|
|
|
1013 |
}
|
|
|
1014 |
|
|
|
1015 |
/*
|
|
|
1016 |
Return the FS type, status and size info
|
|
|
1017 |
*/
|
|
|
1018 |
void AsyncFsWebServer::handleFsStatus(AsyncWebServerRequest *request)
|
|
|
1019 |
{
|
|
|
1020 |
log_debug("handleStatus");
|
|
|
1021 |
fsInfo_t info = {1024, 1024, "ESP Filesystem"};
|
|
|
1022 |
#ifdef ESP8266
|
|
|
1023 |
FSInfo fs_info;
|
|
|
1024 |
m_filesystem->info(fs_info);
|
|
|
1025 |
info.totalBytes = fs_info.totalBytes;
|
|
|
1026 |
info.usedBytes = fs_info.usedBytes;
|
|
|
1027 |
#endif
|
|
|
1028 |
if (getFsInfo != nullptr) {
|
|
|
1029 |
getFsInfo(&info);
|
|
|
1030 |
}
|
|
|
1031 |
CJSON::Json doc;
|
|
|
1032 |
doc.setString("type", info.fsName);
|
|
|
1033 |
doc.setString("isOk", m_filesystem_ok ? "true" : "false");
|
|
|
1034 |
|
|
|
1035 |
if (m_filesystem_ok) {
|
|
|
1036 |
IPAddress ip = (WiFi.status() == WL_CONNECTED) ? WiFi.localIP() : WiFi.softAPIP();
|
|
|
1037 |
doc.setString("totalBytes", String(info.totalBytes));
|
|
|
1038 |
doc.setString("usedBytes", String(info.usedBytes));
|
|
|
1039 |
doc.setString("mode", WiFi.status() == WL_CONNECTED ? "Station" : "Access Point");
|
|
|
1040 |
doc.setString("ssid", WiFi.SSID());
|
|
|
1041 |
doc.setString("ip", ip.toString());
|
|
|
1042 |
}
|
|
|
1043 |
doc.setString("unsupportedFiles", "");
|
|
|
1044 |
String json = doc.serialize();
|
|
|
1045 |
request->send(200, "application/json", json);
|
|
|
1046 |
}
|
|
|
1047 |
#endif // ESP_FS_WS_EDIT
|
|
|
1048 |
|