代码之家  ›  专栏  ›  技术社区  ›  Codebeat

esp:操作系统自动登录/接受消息,并重定向至公共WiFi门户接入点等页面

  •  0
  • Codebeat  · 技术社区  · 7 年前

    例子:

    我不知道你怎么称呼这个功能,所以我试着解释一下。例如,当您将手机连接到公共WiFi热点/接入点时,您会收到操作系统发出的消息,表示需要接受条款/条件,或者需要登录才能使用该连接。当您单击此消息时,它将打开/A浏览器并启动登录页。

    他们是怎么做到的?这是一个HTTPS功能吗?


    更新: (1)

    它被称为捕获门户:

    强制门户技术强制网络上的HTTP客户端在正常使用Internet之前查看特殊的网页(通常用于身份验证)。捕获门户将Web浏览器转换为身份验证设备。这是通过截取大多数数据包来完成的,不管地址或端口如何,直到用户打开浏览器并尝试访问Web。此时,浏览器被重定向到可能需要身份验证和/或付款的网页,或者只显示可接受的使用策略并要求用户同意。在许多Wi-Fi热点上都使用了强制入口,并且可以用来控制有线接入(例如公寓、酒店客房、商务中心、“开放式”以太网插孔)。


    情况:

    我想对一个esp(esp32/esp8266)做同样的事情,因为我开发了一个可以通过wifi配置的设备。当用户连接到设备时,它将是非常用户友好的,不需要先打开浏览器,输入IP地址等。只要一条消息,用户可以点击它,一个主页就会启动。

    在AP模式下,我打开了DNS并将所有内容重定向到主页,但这不是最好的解决方案(有时也不起作用)。为了让您了解我在做什么,这里是一些代码,我的库的一部分:

    bool suStartAccessPointNetwork( String  sSSIDNetworkName, String   sSSIDPassword,
                                    IPAddress sStaticIpAddress,
                                    uint16_t iDnsPort = 0,
                                    uint8_t iTries = 20, 
                                    uint16_t iDelay = 500 
                                  )
    { 
      WiFi.disconnect();
      suSetNetworkAccessPointMode();
      delay( 1000 );
      WiFi.softAPConfig( sStaticIpAddress,
                           sStaticIpAddress,
                           IPAddress( 255, 255, 255, 0 )
                         ); 
    
      SU_WIFI_AP_ESTABLISHED = WiFi.softAP( sSSIDNetworkName.c_str(), sSSIDPassword.c_str() );
    
      if( iDnsPort > 0 )
      {
           static SUDNSServer* pDnsServer = NULLPTR;
    
           if( !pDnsServer )
            { pDnsServer = new SUDNSServer(); }
    
           // modify TTL, default is 60 seconds
           pDnsServer->setTTL(60);
    
           // set which return code will be used for all other domains (e.g. sending
           // ServerFailure instead of NonExistentDomain will reduce number of queries
           // sent by clients)
           // default is DNSReplyCode::NonExistentDomain
           pDnsServer->setErrorReplyCode( DNSReplyCode::NoError );
           pDnsServer->start( iDnsPort, 
                            //SCDMS_MDNS_ACCESS_NAME, 
                            "*", 
                            sStaticIpAddress
                           );
    
           Serial.print("DNS server started at " );
           Serial.println( sStaticIpAddress ); 
    
           suSetDnsServer( pDnsServer );
      } 
    
    
      Serial.print("[AP]:");
    
      bool bConnected = false;
    
      if( iTries == 0 )
       { ++iTries; } 
    
       // Try to connect, give WiFi 10 seconds to launch a network as access point.
      while( !(bConnected = suIsNetworkEstablished()) && (iTries--) )
      {
          Serial.print('.');
          //Serial.print(WiFi.status());
          delay(500);
      }
    
      if( bConnected )
       { SU_WIFI_AP_ESTABLISHED = bConnected; }
    
      Serial.println( bConnected?" ((o))":" o?" );
    
    
      return bConnected;
    }
    

    问题:

    如何实现上述示例中描述的功能?有什么想法吗?有可能吗?

    ————————————————————-

    更新2:

    @Gre_Gor在知道服务的名称后,也提出了同样的例子(另请参见更新1)。像例子中那样实现URL路径等,并不像上面描述的那样工作。好的,尝试完整的例子,同样的结果,没有操作系统弹出窗口/消息,没有重定向。在浏览器中输入IP地址,这与我已经使用的方法相同。

    我尝试过的扩展演示之一:

    #include <ESP8266WiFi.h>
    #include <WiFiClient.h>
    #include <ESP8266WebServer.h>
    #include <DNSServer.h>
    #include <ESP8266mDNS.h>
    #include <EEPROM.h>
    
    /*
       This example serves a "hello world" on a WLAN and a SoftAP at the same time.
       The SoftAP allow you to configure WLAN parameters at run time. They are not setup in the sketch but saved on EEPROM.
       Connect your computer or cell phone to wifi network ESP_ap with password 12345678. A popup may appear and it allow you to go to WLAN config. If it does not then navigate to http://192.168.4.1/wifi and config it there.
       Then wait for the module to connect to your wifi and take note of the WLAN IP it got. Then you can disconnect from ESP_ap and return to your regular WLAN.
       Now the ESP8266 is in your network. You can reach it through http://192.168.x.x/ (the IP you took note of) or maybe at http://esp8266.local too.
       This is a captive portal because through the softAP it will redirect any http request to http://192.168.4.1/
    */
    
    /* Set these to your desired softAP credentials. They are not configurable at runtime */
    #ifndef APSSID
    #define APSSID "ESP_ap"
    #define APPSK  "12345678"
    #endif
    
    const char *softAP_ssid = APSSID;
    const char *softAP_password = APPSK;
    
    /* hostname for mDNS. Should work at least on windows. Try http://esp8266.local */
    const char *myHostname = "esp8266";
    
    /* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */
    char ssid[32] = "";
    char password[32] = "";
    
    // DNS server
    const byte DNS_PORT = 53;
    DNSServer dnsServer;
    
    // Web server
    ESP8266WebServer server(80);
    
    /* Soft AP network parameters */
    IPAddress apIP(192, 168, 4, 1);
    IPAddress netMsk(255, 255, 255, 0);
    
    
    /** Should I connect to WLAN asap? */
    boolean connect;
    
    /** Last time I tried to connect to WLAN */
    unsigned long lastConnectTry = 0;
    
    /** Current WLAN status */
    unsigned int status = WL_IDLE_STATUS;
    
    /** Is this an IP? */
    boolean isIp(String str) {
      for (size_t i = 0; i < str.length(); i++) {
        int c = str.charAt(i);
        if (c != '.' && (c < '0' || c > '9')) {
          return false;
        }
      }
      return true;
    }
    
    /** IP to String? */
    String toStringIp(IPAddress ip) {
      String res = "";
      for (int i = 0; i < 3; i++) {
        res += String((ip >> (8 * i)) & 0xFF) + ".";
      }
      res += String(((ip >> 8 * 3)) & 0xFF);
      return res;
    }
    
    /** Load WLAN credentials from EEPROM */
    void loadCredentials() {
      EEPROM.begin(512);
      EEPROM.get(0, ssid);
      EEPROM.get(0 + sizeof(ssid), password);
      char ok[2 + 1];
      EEPROM.get(0 + sizeof(ssid) + sizeof(password), ok);
      EEPROM.end();
      if (String(ok) != String("OK")) {
        ssid[0] = 0;
        password[0] = 0;
      }
      Serial.println("Recovered credentials:");
      Serial.println(ssid);
      Serial.println(strlen(password) > 0 ? "********" : "<no password>");
    }
    
    /** Store WLAN credentials to EEPROM */
    void saveCredentials() {
      EEPROM.begin(512);
      EEPROM.put(0, ssid);
      EEPROM.put(0 + sizeof(ssid), password);
      char ok[2 + 1] = "OK";
      EEPROM.put(0 + sizeof(ssid) + sizeof(password), ok);
      EEPROM.commit();
      EEPROM.end();
    }
    
    /** Handle root or redirect to captive portal */
    void handleRoot() {
      if (captivePortal()) { // If caprive portal redirect instead of displaying the page.
        return;
      }
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
    
      String Page;
      Page += F(
                "<html><head></head><body>"
                "<h1>HELLO WORLD!!</h1>");
      if (server.client().localIP() == apIP) {
        Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
      } else {
        Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
      }
      Page += F(
                "<p>You may want to <a href='/wifi'>config the wifi connection</a>.</p>"
                "</body></html>");
    
      server.send(200, "text/html", Page);
    }
    
    /** Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */
    boolean captivePortal() {
      if (!isIp(server.hostHeader()) && server.hostHeader() != (String(myHostname) + ".local")) {
        Serial.println("Request redirected to captive portal");
        server.sendHeader("Location", String("http://") + toStringIp(server.client().localIP()), true);
        server.send(302, "text/plain", "");   // Empty content inhibits Content-length header so we have to close the socket ourselves.
        server.client().stop(); // Stop is needed because we sent no content length
        return true;
      }
      return false;
    }
    
    /** Wifi config page handler */
    void handleWifi() {
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
    
      String Page;
      Page += F(
                "<html><head></head><body>"
                "<h1>Wifi config</h1>");
      if (server.client().localIP() == apIP) {
        Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
      } else {
        Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
      }
      Page +=
        String(F(
                 "\r\n<br />"
                 "<table><tr><th align='left'>SoftAP config</th></tr>"
                 "<tr><td>SSID ")) +
        String(softAP_ssid) +
        F("</td></tr>"
          "<tr><td>IP ") +
        toStringIp(WiFi.softAPIP()) +
        F("</td></tr>"
          "</table>"
          "\r\n<br />"
          "<table><tr><th align='left'>WLAN config</th></tr>"
          "<tr><td>SSID ") +
        String(ssid) +
        F("</td></tr>"
          "<tr><td>IP ") +
        toStringIp(WiFi.localIP()) +
        F("</td></tr>"
          "</table>"
          "\r\n<br />"
          "<table><tr><th align='left'>WLAN list (refresh if any missing)</th></tr>");
      Serial.println("scan start");
      int n = WiFi.scanNetworks();
      Serial.println("scan done");
      if (n > 0) {
        for (int i = 0; i < n; i++) {
          Page += String(F("\r\n<tr><td>SSID ")) + WiFi.SSID(i) + ((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? F(" ") : F(" *")) + F(" (") + WiFi.RSSI(i) + F(")</td></tr>");
        }
      } else {
        Page += F("<tr><td>No WLAN found</td></tr>");
      }
      Page += F(
                "</table>"
                "\r\n<br /><form method='POST' action='wifisave'><h4>Connect to network:</h4>"
                "<input type='text' placeholder='network' name='n'/>"
                "<br /><input type='password' placeholder='password' name='p'/>"
                "<br /><input type='submit' value='Connect/Disconnect'/></form>"
                "<p>You may want to <a href='/'>return to the home page</a>.</p>"
                "</body></html>");
      server.send(200, "text/html", Page);
      server.client().stop(); // Stop is needed because we sent no content length
    }
    
    /** Handle the WLAN save form and redirect to WLAN config page again */
    void handleWifiSave() {
      Serial.println("wifi save");
      server.arg("n").toCharArray(ssid, sizeof(ssid) - 1);
      server.arg("p").toCharArray(password, sizeof(password) - 1);
      server.sendHeader("Location", "wifi", true);
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
      server.send(302, "text/plain", "");    // Empty content inhibits Content-length header so we have to close the socket ourselves.
      server.client().stop(); // Stop is needed because we sent no content length
      saveCredentials();
      connect = strlen(ssid) > 0; // Request WLAN connect with new credentials if there is a SSID
    }
    
    void handleNotFound() {
      if (captivePortal()) { // If caprive portal redirect instead of displaying the error page.
        return;
      }
      String message = F("File Not Found\n\n");
      message += F("URI: ");
      message += server.uri();
      message += F("\nMethod: ");
      message += (server.method() == HTTP_GET) ? "GET" : "POST";
      message += F("\nArguments: ");
      message += server.args();
      message += F("\n");
    
      for (uint8_t i = 0; i < server.args(); i++) {
        message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\n");
      }
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
      server.send(404, "text/plain", message);
    }
    
    void setup() {
      delay(1000);
      Serial.begin(9600);
      Serial.println();
      Serial.println("Configuring access point...");
      /* You can remove the password parameter if you want the AP to be open. */
      WiFi.softAPConfig(apIP, apIP, netMsk);
      WiFi.softAP(softAP_ssid, softAP_password);
      delay(500); // Without delay I've seen the IP address blank
      Serial.print("AP IP address: ");
      Serial.println(WiFi.softAPIP());
    
      /* Setup the DNS server redirecting all the domains to the apIP */
      dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
      dnsServer.start(DNS_PORT, "*", apIP);
    
      /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
      server.on("/", handleRoot);
      server.on("/wifi", handleWifi);
      server.on("/wifisave", handleWifiSave);
      server.on("/generate_204", handleRoot);  //Android captive portal. Maybe not needed. Might be handled by notFound handler.
      server.on("/fwlink", handleRoot);  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
      server.onNotFound(handleNotFound);
      server.begin(); // Web server start
      Serial.println("HTTP server started");
      loadCredentials(); // Load WLAN credentials from network
      connect = strlen(ssid) > 0; // Request WLAN connect if there is a SSID
    }
    
    void connectWifi() {
      Serial.println("Connecting as wifi client...");
      WiFi.disconnect();
      WiFi.begin(ssid, password);
      int connRes = WiFi.waitForConnectResult();
      Serial.print("connRes: ");
      Serial.println(connRes);
    }
    
    void loop() {
      if (connect) {
        Serial.println("Connect requested");
        connect = false;
        connectWifi();
        lastConnectTry = millis();
      }
      {
        unsigned int s = WiFi.status();
        if (s == 0 && millis() > (lastConnectTry + 60000)) {
          /* If WLAN disconnected and idle try to connect */
          /* Don't set retry time too low as retry interfere the softAP operation */
          connect = true;
        }
        if (status != s) { // WLAN status change
          Serial.print("Status: ");
          Serial.println(s);
          status = s;
          if (s == WL_CONNECTED) {
            /* Just connected to WLAN */
            Serial.println("");
            Serial.print("Connected to ");
            Serial.println(ssid);
            Serial.print("IP address: ");
            Serial.println(WiFi.localIP());
    
            // Setup MDNS responder
            if (!MDNS.begin(myHostname)) {
              Serial.println("Error setting up MDNS responder!");
            } else {
              Serial.println("mDNS responder started");
              // Add service to MDNS-SD
              MDNS.addService("http", "tcp", 80);
            }
          } else if (s == WL_NO_SSID_AVAIL) {
            WiFi.disconnect();
          }
        }
        if (s == WL_CONNECTED) {
          MDNS.update();
        }
      }
      // Do work:
      //DNS
      dnsServer.processNextRequest();
      //HTTP
      server.handleClient();
    }
    
    1 回复  |  直到 7 年前
        1
  •  0
  •   Codebeat    7 年前

    发现这一点: https://www.esp8266.com/viewtopic.php?f=6&t=15993

    Eduperez的这篇文章:


    “Android设备将谷歌的dnss硬编码,他们将 始终使用8.8.8.8 以及8.8.4.4,尽管DHPC服务器可能会告诉他们使用什么。您需要配置网关以将所有传出流量重定向到端口53到DNS。”


    这就产生了想法,使用 8.8.8.8作为IP地址和DNS服务器地址 . 塔达!真是魅力四射!

    因此,这张草图将起作用:

    #include <ESP8266WiFi.h>
    #include <WiFiClient.h>
    #include <ESP8266WebServer.h>
    #include <DNSServer.h>
    #include <ESP8266mDNS.h>
    #include <EEPROM.h>
    
    /*
       This example serves a "hello world" on a WLAN and a SoftAP at the same time.
       The SoftAP allow you to configure WLAN parameters at run time. They are not setup in the sketch but saved on EEPROM.
       Connect your computer or cell phone to wifi network ESP_ap with password 12345678. A popup may appear and it allow you to go to WLAN config. If it does not then navigate to http://192.168.4.1/wifi and config it there.
       Then wait for the module to connect to your wifi and take note of the WLAN IP it got. Then you can disconnect from ESP_ap and return to your regular WLAN.
       Now the ESP8266 is in your network. You can reach it through http://192.168.x.x/ (the IP you took note of) or maybe at http://esp8266.local too.
       This is a captive portal because through the softAP it will redirect any http request to http://192.168.4.1/
    */
    
    /* Set these to your desired softAP credentials. They are not configurable at runtime */
    #ifndef APSSID
    #define APSSID "TheGeekMan"
    #define APPSK  "12345678"
    #endif
    
    const char *softAP_ssid = APSSID;
    const char *softAP_password = APPSK;
    
    /* hostname for mDNS. Should work at least on windows. Try http://esp8266.local */
    const char *myHostname = "thegeekman";
    
    /* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */
    char ssid[32] = "";
    char password[32] = "";
    
    // DNS server
    const byte DNS_PORT = 53;
    DNSServer dnsServer;
    
    // Web server
    ESP8266WebServer server(80);
    
    /* Soft AP network parameters */
    //IPAddress apIP(192, 168, 4, 1);
    IPAddress apIP(8, 8, 8, 8);
    IPAddress netMsk(255, 255, 255, 0);
    
    
    /** Should I connect to WLAN asap? */
    boolean connect;
    
    /** Last time I tried to connect to WLAN */
    unsigned long lastConnectTry = 0;
    
    /** Current WLAN status */
    unsigned int status = WL_IDLE_STATUS;
    
    /** Is this an IP? */
    boolean isIp(String str) {
      for (size_t i = 0; i < str.length(); i++) {
        int c = str.charAt(i);
        if (c != '.' && (c < '0' || c > '9')) {
          return false;
        }
      }
      return true;
    }
    
    /** IP to String? */
    String toStringIp(IPAddress ip) {
      String res = "";
      for (int i = 0; i < 3; i++) {
        res += String((ip >> (8 * i)) & 0xFF) + ".";
      }
      res += String(((ip >> 8 * 3)) & 0xFF);
      return res;
    }
    
    /** Load WLAN credentials from EEPROM */
    void loadCredentials() {
      EEPROM.begin(512);
      EEPROM.get(0, ssid);
      EEPROM.get(0 + sizeof(ssid), password);
      char ok[2 + 1];
      EEPROM.get(0 + sizeof(ssid) + sizeof(password), ok);
      EEPROM.end();
      if (String(ok) != String("OK")) {
        ssid[0] = 0;
        password[0] = 0;
      }
      Serial.println("Recovered credentials:");
      Serial.println(ssid);
      Serial.println(strlen(password) > 0 ? "********" : "<no password>");
    }
    
    /** Store WLAN credentials to EEPROM */
    void saveCredentials() {
      EEPROM.begin(512);
      EEPROM.put(0, ssid);
      EEPROM.put(0 + sizeof(ssid), password);
      char ok[2 + 1] = "OK";
      EEPROM.put(0 + sizeof(ssid) + sizeof(password), ok);
      EEPROM.commit();
      EEPROM.end();
    }
    
    /** Handle root or redirect to captive portal */
    void handleRoot() {
      if (captivePortal()) { // If caprive portal redirect instead of displaying the page.
        return;
      }
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
    
      String Page;
      Page += F(
                "<html><head></head><body>"
                "<h1>HELLO WORLD!!</h1>");
      if (server.client().localIP() == apIP) {
        Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
      } else {
        Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
      }
      Page += F(
                "<p>You may want to <a href='/wifi'>config the wifi connection</a>.</p>"
                "</body></html>");
    
      server.send(200, "text/html", Page);
    }
    
    /** Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */
    boolean captivePortal() {
      if (!isIp(server.hostHeader()) && server.hostHeader() != (String(myHostname) + ".local")) {
        Serial.println("Request redirected to captive portal");
        server.sendHeader("Location", String("http://") + toStringIp(server.client().localIP()), true);
        server.send(302, "text/plain", "");   // Empty content inhibits Content-length header so we have to close the socket ourselves.
        server.client().stop(); // Stop is needed because we sent no content length
        return true;
      }
      return false;
    }
    
    /** Wifi config page handler */
    void handleWifi() {
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
    
      String Page;
      Page += F(
                "<html><head></head><body>"
                "<h1>Wifi config</h1>");
      if (server.client().localIP() == apIP) {
        Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
      } else {
        Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
      }
      Page +=
        String(F(
                 "\r\n<br />"
                 "<table><tr><th align='left'>SoftAP config</th></tr>"
                 "<tr><td>SSID ")) +
        String(softAP_ssid) +
        F("</td></tr>"
          "<tr><td>IP ") +
        toStringIp(WiFi.softAPIP()) +
        F("</td></tr>"
          "</table>"
          "\r\n<br />"
          "<table><tr><th align='left'>WLAN config</th></tr>"
          "<tr><td>SSID ") +
        String(ssid) +
        F("</td></tr>"
          "<tr><td>IP ") +
        toStringIp(WiFi.localIP()) +
        F("</td></tr>"
          "</table>"
          "\r\n<br />"
          "<table><tr><th align='left'>WLAN list (refresh if any missing)</th></tr>");
      Serial.println("scan start");
      int n = WiFi.scanNetworks();
      Serial.println("scan done");
      if (n > 0) {
        for (int i = 0; i < n; i++) {
          Page += String(F("\r\n<tr><td>SSID ")) + WiFi.SSID(i) + ((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? F(" ") : F(" *")) + F(" (") + WiFi.RSSI(i) + F(")</td></tr>");
        }
      } else {
        Page += F("<tr><td>No WLAN found</td></tr>");
      }
      Page += F(
                "</table>"
                "\r\n<br /><form method='POST' action='wifisave'><h4>Connect to network:</h4>"
                "<input type='text' placeholder='network' name='n'/>"
                "<br /><input type='password' placeholder='password' name='p'/>"
                "<br /><input type='submit' value='Connect/Disconnect'/></form>"
                "<p>You may want to <a href='/'>return to the home page</a>.</p>"
                "</body></html>");
      server.send(200, "text/html", Page);
      server.client().stop(); // Stop is needed because we sent no content length
    }
    
    /** Handle the WLAN save form and redirect to WLAN config page again */
    void handleWifiSave() {
      Serial.println("wifi save");
      server.arg("n").toCharArray(ssid, sizeof(ssid) - 1);
      server.arg("p").toCharArray(password, sizeof(password) - 1);
      server.sendHeader("Location", "wifi", true);
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
      server.send(302, "text/plain", "");    // Empty content inhibits Content-length header so we have to close the socket ourselves.
      server.client().stop(); // Stop is needed because we sent no content length
      saveCredentials();
      connect = strlen(ssid) > 0; // Request WLAN connect with new credentials if there is a SSID
    }
    
    void handleNotFound() {
      if (captivePortal()) { // If caprive portal redirect instead of displaying the error page.
        return;
      }
      String message = F("File Not Found\n\n");
      message += F("URI: ");
      message += server.uri();
      message += F("\nMethod: ");
      message += (server.method() == HTTP_GET) ? "GET" : "POST";
      message += F("\nArguments: ");
      message += server.args();
      message += F("\n");
    
      for (uint8_t i = 0; i < server.args(); i++) {
        message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\n");
      }
      server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
      server.sendHeader("Pragma", "no-cache");
      server.sendHeader("Expires", "-1");
      server.send(404, "text/plain", message);
    }
    
    void setup() {
      delay(1000);
      Serial.begin(9600);
      Serial.println();
      Serial.println("Configuring access point...");
      /* You can remove the password parameter if you want the AP to be open. */
      WiFi.softAPConfig(apIP, apIP, netMsk);
      WiFi.softAP(softAP_ssid, softAP_password);
      delay(500); // Without delay I've seen the IP address blank
      Serial.print("AP IP address: ");
      Serial.println(WiFi.softAPIP());
    
      /* Setup the DNS server redirecting all the domains to the apIP */
      dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
      dnsServer.start(DNS_PORT, "*", apIP);
    
      /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
      server.on("/", handleRoot);
      server.on("/wifi", handleWifi);
      server.on("/wifisave", handleWifiSave);
      server.on("/generate_204", handleRoot);  //Android captive portal. Maybe not needed. Might be handled by notFound handler.
      server.on("/fwlink", handleRoot);  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
      server.onNotFound(handleNotFound);
      server.begin(); // Web server start
      Serial.println("HTTP server started");
      loadCredentials(); // Load WLAN credentials from network
      connect = strlen(ssid) > 0; // Request WLAN connect if there is a SSID
    }
    
    void connectWifi() {
      Serial.println("Connecting as wifi client...");
      WiFi.disconnect();
      WiFi.begin(ssid, password);
      int connRes = WiFi.waitForConnectResult();
      Serial.print("connRes: ");
      Serial.println(connRes);
    }
    
    void loop() {
      if (connect) {
        Serial.println("Connect requested");
        connect = false;
        connectWifi();
        lastConnectTry = millis();
      }
      {
        unsigned int s = WiFi.status();
        if (s == 0 && millis() > (lastConnectTry + 60000)) {
          /* If WLAN disconnected and idle try to connect */
          /* Don't set retry time too low as retry interfere the softAP operation */
          connect = true;
        }
        if (status != s) { // WLAN status change
          Serial.print("Status: ");
          Serial.println(s);
          status = s;
          if (s == WL_CONNECTED) {
            /* Just connected to WLAN */
            Serial.println("");
            Serial.print("Connected to ");
            Serial.println(ssid);
            Serial.print("IP address: ");
            Serial.println(WiFi.localIP());
    
            // Setup MDNS responder
            if (!MDNS.begin(myHostname)) {
              Serial.println("Error setting up MDNS responder!");
            } else {
              Serial.println("mDNS responder started");
              // Add service to MDNS-SD
              MDNS.addService("http", "tcp", 80);
            }
          } else if (s == WL_NO_SSID_AVAIL) {
            WiFi.disconnect();
          }
        }
        if (s == WL_CONNECTED) {
          MDNS.update();
        }
      }
      // Do work:
      //DNS
      dnsServer.processNextRequest();
      //HTTP
      server.handleClient();
    }
    

    Example