ESP32 Captive Portal

Captive portal - Wikipedia

What is a captive portal?

Have you ever tried to connect to the WiFi at a hotel or at an airport, and immediately, a webpage opened up, asking for your mobile number/ other details? This web page is called the captive portal. You are obliged to go through this webpage to gain further access, say, to the internet.

Now, ESP32 can also create its own Wi-Fi field in the Access Point mode. Of course, since the ESP32 itself is not connected to the internet, you will not be able to connect to the internet when you connect to ESP32’s Wi-Fi field. However, there are several other reasons for which you may want to connect to ESP32’s Wi-Fi field.

A common application is configuration. If your product is based on ESP32 and is going to be deployed at a remote location with unknown WiFi credentials, you can

  1. Configure your ESP32 to start off in the Access Point mode
  2. Make the user to connect to ESP32’s Wi-Fi field
  3. Show him/her a Captive Portal webpage as soon the connection is established, prompting the user to enter the Wi-Fi credentials

Once the user enters the Wi-Fi credentials, the ESP32 can switch from the Access Point mode to the station mode and connect to the available Wi-Fi network using the supplied credentials. This way you can deploy the ESP32 in any remote location without hard-coding the Wi-Fi credentials.

#include <ESPmDNS.h>
#include <DNSServer.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include "ESPAsyncWebServer.h"

const byte DNS_PORT = 53;
IPAddress apIP(8,8,4,4);
IPAddress subnet(255, 255, 255, 0);

DNSServer dnsServer;
AsyncWebServer server(80);

class CaptiveRequestHandler : public AsyncWebHandler {
public:
  CaptiveRequestHandler() {}
  virtual ~CaptiveRequestHandler() {}

  bool canHandle(AsyncWebServerRequest *request){
    //request->addInterestingHeader("ANY");
    return true;
  }

  void handleRequest(AsyncWebServerRequest *request) {
    AsyncResponseStream *response = request->beginResponseStream("text/html");
    response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
    response->print("<p>CAPTIVE PORTAL</p>");
    response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
    response->printf("<p>Try opening <a href='http://%s/app'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
    response->print("</body></html>");
    request->send(response);
  }
};


void setup(){
  Serial.begin(115200);
  WiFi.mode(WIFI_AP);
  WiFi.softAP("ESP32Captive");
  WiFi.softAPConfig(apIP, apIP, subnet);
  dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());

  if(!MDNS.begin("makexyzfun")) {
     Serial.println("Error starting mDNS");
     return;
  }

  
  server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER);//only when requested from AP
  server.on("/app", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send(200, "text/plain", "Hello, world");
  });
  server.onNotFound([](AsyncWebServerRequest *request){
    request->send(404);
  });
  server.begin();
  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);
}

void loop(){
  dnsServer.processNextRequest();
}