me-no-dev / ESPAsyncWebServer

Async Web Server for ESP8266 and ESP32
3.59k stars 1.17k forks source link

Handling a multipage Captive Portal. #1291

Open ambarusa opened 1 year ago

ambarusa commented 1 year ago

Hello. In my current project I am bringing up a webserver with multiple locations ("/", "/settings", ...) in STA or AP mode, depending if there are credentials stored. It functions as the WiFiManager, but with the same website in both modes. This would allow the user to set some properties without connecting the device to a network.

I am looking for a solution now to bring up the website as a Captive Portal in AP mode. I started out from the published example, but the first thing I realized was that dnsServer.start(53, "*", WiFi.softAPIP()); will redirect to the root location all the time, especially when the users clicks on the Settings menu item.

This is function to start my webserver after it's connected to an AP or making an AP. It would be the next step to make the CaptiveRequestHandler class reuse this:

void Webserver_start()
{
    webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/index.html", "text/html", false, processor); });
    webserver.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/settings.html", "text/html", false, processor); });
    webserver.on("/bootstrap.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/bootstrap.min.css", "text/css"); });
    webserver.on("/bootstrap.min.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/bootstrap.min.js", "text/javascript"); });
    webserver.on("/index.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/index.js", "text/javascript"); });
    webserver.on("/settings.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/settings.js", "text/javascript"); });
    webserver.on("/save_config", HTTP_POST, onSaveConfig);
    webserver.on("/save_config", HTTP_GET, onSaveConfig);
    webserver.on("/reset_config", HTTP_GET, onResetConfig);
    webserver.onNotFound([](AsyncWebServerRequest *request)
                         { request->send(404, "text/plain", "Not found"); });

    Serial.println("Webserver: Starting the webserver");
    webserver.begin();
}

Does anyone know how to make the DNSServer, or the CaptiveRequestHandler class work this way? If you need any clarifications, please let me know!

ambarusa commented 1 year ago

Here's the link to the CaptivePortal example, as the starting point: https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/examples/CaptivePortal/CaptivePortal.ino

ambarusa commented 1 year ago

After many hours of research, I can say that my assumption about DNSServer was completely wrong, I had a bad experience due to the browser on my PC. However, the example will not work with more complex websites, including websockets just like that. I expanded the CaptiveRequestHandler class like this:

class CaptiveRequestHandler : public AsyncWebHandler
{
public:
    CaptiveRequestHandler()
    {
        Webserver_configure();
        webserver.onNotFound([](AsyncWebServerRequest *request)
                             {  AsyncWebServerResponse *response = request->beginResponse(302);
                                response->addHeader(F("Location"), F("http://4.3.2.1"));
                                request->send(response); });
    }
    virtual ~CaptiveRequestHandler() {}

    bool canHandle(AsyncWebServerRequest *request)
    {
        return true;
    }

    void handleRequest(AsyncWebServerRequest *request)
    {
        AsyncWebServerResponse *response = request->beginResponse(302);
        response->addHeader(F("Location"), F("http://4.3.2.1"));
        request->send(response);
    }
};

void Webserver_init()
{
    if (LittleFS.begin())
        Serial.println("Webserver: LittleFS mounted successfully");
    else
        Serial.println("Webserver: An error has occurred while mounting LittleFS");
}

void Webserver_configure()
{
    webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
                 {  request->send(LittleFS, "/index.html", "text/html", false, processor); });

    webserver.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/settings.html", "text/html", false, processor); });

    webserver.on("/bootstrap.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/bootstrap.min.css", "text/css"); });

    webserver.on("/bootstrap.min.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/bootstrap.min.js", "text/javascript"); });

    webserver.on("/index.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/index.js", "text/javascript", false, processor); });

    webserver.on("/settings.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/settings.js", "text/javascript", false, processor); });

    webserver.on("/chart.min.js", HTTP_GET, [](AsyncWebServerRequest *request)
                 { request->send(LittleFS, "/chart.min.js", "text/javascript"); });

    webserver.on("/save_config", HTTP_POST, onSaveConfig);

    webserver.on("/reset_config", HTTP_GET, onResetConfig);

    webserver.onNotFound([](AsyncWebServerRequest *request)
                         { request->send(404, "text/plain", "Not found"); });
}

void Webserver_start()
{
    websocket.onEvent(onEvent);
    webserver.addHandler(&websocket);
    webserver.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER);
    webserver.begin();
}

When it's in AP mode, this implementation will make a redirect to the ESP, when there's a request or it's a notFound. This way the websocket's will also work, if you use something like this in the javascript: var gateway = 'ws://${window.location.hostname}/ws';

This is how I reused my code, mentioned in my initial description, and keep in mind that this code is just in a test form.

ambarusa commented 1 year ago

The reason I am using a different IP, than 192.168.4.1 is to make the captive portal mechanism compatible with Android devices. On iOS it is a bit more complicated, the first example will work, the second one will not. The only change is that in the first case I am making sure that Http code 200 is sent:

    webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
                 {  request->send(200, "text/html", someBasicHtml); });

    webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
                 {  request->send(LittleFS, "/index.html", "text/html", false, processor); });

A big downside in this is that I cannot add a processor callback function to the first example, it needs a big workaround, compared to the second one, which is so elegant.

@me-no-dev Can you take a look to this case, please?

ambarusa commented 1 year ago

I made some progress, created a char array of my HTML webpage, and now the page is coming up even on iOS:

    webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
                 {  if (CaptivePortalHandled(request))
                        return;
                    AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", PAGE_index);
                    response->addHeader(F("Cache-Control"),"no-cache");
                    request->send(response);
                        });

If I add the processor as parameter, it will not work anymore.

ambarusa commented 1 year ago

In any case if the processor is added, the Captive Portal is not working anymore. I also get tons of Exception(3), Exception(9) or Exception(28) if the webpage is loaded from a String.

It is a huge drawback, because without the processor the webpage is not loaded with the current settings, there is a visible lag until the websocket message is read out.

stale[bot] commented 1 year ago

[STALE_SET] This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

jaymedavis commented 3 weeks ago

For anyone else that stumbles by, I was on the struggle bus with this issue as well. For whatever reason, a form POST would not work for me, so I had to do everything with GETs. I don't like it, but it works. Once you put in the CaptiveRequestHandler, it takes over all of your requests, so doing webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request) ... didn't help.

I did find a way around it though. You can just process everything in the CaptiveRequestHandler. Here is what I am using to build a wifi page where the user enters their credentials. With this method, inside your if statement looking at the path, be sure to always request->send to stop the function from processing any further. I thought this was cleaner than a bunch of else statements.

  <form action="/wifi" method="get">
      <input type="text" id="ssid" name="ssid" placeholder="SSID">
      <input type="text" id="password" name="password" placeholder="Password">
      <input type="submit" value="Submit">
  </form>
void CaptiveRequestHandler::handleRequest(AsyncWebServerRequest *request) {
  auto path = request->url();

  if (path == "/wifi") {
    if (request->hasParam("ssid")) {
      auto ssid = request->getParam("ssid")->value();
      Serial.println(ssid);
    }

    if (request->hasParam("password")) {
      auto password = request->getParam("password")->value();
      Serial.println(password);
    }

    request->send_P(200, "text/html", index_html);
  }

  // add other checks, for example
  if (path == "/blah") {
    request->send_P(200, "text/html", blah_html);
  }

  // default page
  request->send_P(200, "text/html", index_html);
}

I hope this helps someone!

stale[bot] commented 3 weeks ago

[STALE_CLR] This issue has been removed from the stale queue. Please ensure activity to keep it openin the future.