Subversion Repositories ESP8266_P1_Meter

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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