diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/NAPTCaptivePortal.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/NAPTCaptivePortal.ino new file mode 100644 index 0000000000..cb65f518ef --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/NAPTCaptivePortal.ino @@ -0,0 +1,391 @@ +/* + This example shows the use of the 'DNS forwarder' feature in the DNSServer. + It does so by combining two examples CaptivePortalAdvanced and + RangeExtender-NAPT. Additionally the CaptivePortalAdvanced part has a few + upgrades to the HTML presentation to improve readability and ease of use on + mobile devices. + + Also for an example of using HTML chunked response, see handleWifi() in + handleHttp.ino. + + This example starts up in Captive Portal mode by default. + It starts the SoftAP and NAPT w/o connecting the WLAN side. + + You connect your computer or mobile device to the WiFi Network 'MagicPortal' + password 'ShowTime'. Your device should shortly notify you of a Captive + Portal and the need to login. If it fails to do so in a timely maner, + navigate to http://172.217.28.1/wifi and configure it there. + + Note, until a successful WLAN connection is made all DNS lookups will point + back to the SoftAP at 172.217.28.1. This is the Captive Portal element of + this example. + + Once the WLAN is connected, your device should notify you that you are + connected. This, of course, assumes your WLAN connection has a path to the + Internet. + + At this stage we are no longer running as a Captive Portal, but a regular + NAPT. The DNSServer will be running with the DNS forwarder enabled. The + DNSServer will resolve lookups for 'margicportal' to point to 172.217.28.1 + and all other lookup request will be forwarded to the 1st DNS server that was + in the DHCP response for the WLAN interface. + + You should now be able to access things on the Internet. The ease of access + to devices on your home Network may vary. By IP address it should work. + Access by a hostname - maybe. Some home routers will use the hostname + supplied during DHCP to support a local DNS table; some do not. + + There is an additional possible complication for using the local DNS, the DNS + suffix list, this subject is seldom discussed. It is normally handled + automaticly by the host computers DNS lookup code. For the DHCP case, the + DHCP server will supply a suffix list, if there is one. Then when a name + lookup fails and the name does not have a trailing (.)dot the host computer + will append a suffix from the list and try again, until successful or the + list is exhaused. This topic I fear can become a TL;DR. A quick wrapup by way + of an example. On an Ubuntu system run `nmcli dev show eth0 | grep + IP4\.DOMAIN` that may show you a suffix list. (replace eth0 with your wlan + interface name) Try adding them to the local name you are failing to connect + to. For example, assume 'myhost' fails. You see that 'lan' is in the suffix + list. Try connecting to 'myhost.lan'. + + mDNS names also will not work. We do not have a way to pass those request + back and forth through the NAPT. + + Note if hostnames are going to work for an ESP8266 device on your home + Network, you have to have the call to WiFi.hostname(...) before you call + WiFi.begin(). + + In this example the SoftAP in 'Captive Portal' uses the same public address + that was used in the CaptivePortalAdvanced example. Depending on your devices + you may or may not be successful in using a private address. A previous + PR-author discovered a fix that made the CaptivePortalAdvanced example work + better with Android devices. That fix was to use that public address. At this + time, this PR-author with a different Android device running the latest + version of Android has seen no problems in using either. At least not yet :) + FWIW: My device also works with the original CaptivePortalAdvanced example + when using a private address. I would suggest keeping the public address + for a while. At lest until you are confident everything is working well + before experimenting with a private address. +*/ + + +#if LWIP_FEATURES && !LWIP_IPV6 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAPT 1000 +#define NAPT_PORT 10 + +/* + Some defines for debugging +*/ +#ifdef DEBUG_ESP_PORT +#define CONSOLE DEBUG_ESP_PORT +#else +#define CONSOLE Serial +#endif + +#define _PRINTF(a, ...) printf_P(PSTR(a), ##__VA_ARGS__) +#define _PRINT(a) print(String(F(a))) +#define _PRINTLN(a) println(String(F(a))) +#define _PRINTLN2(a, b) println(String(F(a)) + b) + +#define CONSOLE_PRINTF CONSOLE._PRINTF +#define CONSOLE_PRINT CONSOLE._PRINT +#define CONSOLE_PRINTLN CONSOLE._PRINTLN +#define CONSOLE_PRINTLN2 CONSOLE._PRINTLN2 + +#ifdef DEBUG_SKETCH +#define DEBUG_PRINTF CONSOLE_PRINTF +#define DEBUG_PRINT CONSOLE_PRINT +#define DEBUG_PRINTLN CONSOLE_PRINTLN +#define DEBUG_PRINTLN2 CONSOLE_PRINTLN2 + +#else +#define DEBUG_PRINTF(...) \ + do { \ + } while (false) +#define DEBUG_PRINT(...) \ + do { \ + } while (false) +#define DEBUG_PRINTLN(...) \ + do { \ + } while (false) +#define DEBUG_PRINTLN2(...) \ + do { \ + } while (false) +#endif + + + +/* Set these to your desired softAP credentials. They are not configurable at runtime */ +#ifndef APSSID +#define APSSID "MagicPortal" +#define APPSK "ShowTime" +#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 = "magicportal"; + +/* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */ +char ssid[33] = ""; +char password[65] = ""; +uint8_t bssid[6]; +WiFiEventHandler staModeConnectedHandler; +WiFiEventHandler staModeDisconnectedHandler; + +// DNS server +DNSServer dnsServer; + +// Web server +ESP8266WebServer server(80); + +/* Soft AP network parameters */ +IPAddress apIP(172, 217, 28, 1); +IPAddress netMsk(255, 255, 255, 0); + + +/** Should I connect to WLAN asap? */ +bool connect = false; + +/** Set to true to start WiFi STA at setup time when credentials loaded successfuly from EEPROM */ +/** Set to false to defer WiFi STA until configured through web interface. */ +bool staReady = false; // Don't connect right away + +/** Last time I tried to connect to WLAN */ +unsigned long lastConnectTry = 0; + +/** Current WLAN status */ +unsigned int status = WL_IDLE_STATUS; + +void setup() { + WiFi.persistent(false); // w/o this a flash write occurs at every boot + WiFi.mode(WIFI_OFF); // Prevent use of SDK stored credentials + CONSOLE.begin(115200); + CONSOLE_PRINTLN("\r\n\r\nNAPT with Configuration Portal ..."); + + staModeConnectedHandler = WiFi.onStationModeConnected( + [](const WiFiEventStationModeConnected &data) { + // Keep a copy of the BSSID for the AP that WLAN connects to. + // This is used in the WLAN report on WiFi Details page. + memcpy(bssid, data.bssid, sizeof(bssid)); + }); + + staModeDisconnectedHandler = WiFi.onStationModeDisconnected( + [](const WiFiEventStationModeDisconnected &) { + if (dnsServer.isForwarding()) { + dnsServer.disableForwarder("*"); + dnsServer.setTTL(0); + // Reminder, Serial.println() will not work from these callbacks. + // For debug printf use ets_uart_printf(). + } + }); + + /* + While you can remove the password parameter to make the AP open. + You will be operating with less security and allowing snoopers to see + the credentials you use for your WiFi. + */ + WiFi.softAPConfig(apIP, apIP, netMsk); + WiFi.softAP(softAP_ssid, softAP_password); + // The following comment for delay(500) was committed Aug 19, 2015; is it + // still true? Commented out for verification. - APR 2020 + // delay(500); // Without delay I've seen the IP address blank + CONSOLE_PRINTF("SoftAP '%s' started\r\n", softAP_ssid); + CONSOLE_PRINTLN2(" IP address: ", WiFi.softAPIP().toString()); + + /* Captive portals will usually use a TTL of 0 to avoid DNS cache poisoning. */ + dnsServer.setTTL(0); + + /* Setup the DNS server redirecting all the domains to the apIP */ + dnsServer.start(IANA_DNS_PORT, "*", apIP); + CONSOLE_PRINTLN("DNSServer started:"); + CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off"); + CONSOLE_PRINTF(" Resolve all domain lookups, '%s', to this AP's IP address, '%s' %s.\r\n", + dnsServer.getDomainName().c_str(), + softAP_ssid, + WiFi.softAPIP().toString().c_str()); + CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL()); + + /* + Do some NAPT startup stuff + */ + CONSOLE_PRINTLN("Begin NAPT initialization:"); + CONSOLE_PRINTF(" Heap before NAPT init: %d\r\n", ESP.getFreeHeap()); + + err_t ret = ip_napt_init(NAPT, NAPT_PORT); + CONSOLE_PRINTF(" ip_napt_init(%d,%d): ret=%d (OK=%d)\r\n", NAPT, NAPT_PORT, (int)ret, (int)ERR_OK); + if (ret == ERR_OK) { + ret = ip_napt_enable_no(SOFTAP_IF, 1); + CONSOLE_PRINTF(" ip_napt_enable_no(SOFTAP_IF): ret=%d (OK=%d)\r\n", (int)ret, (int)ERR_OK); + if (ret == ERR_OK) { + CONSOLE_PRINTF(" NAPT AP '%s' started.\r\n", softAP_ssid); + if (WiFi.localIP().isSet()) { + CONSOLE_PRINTF(" It is an extension of '%s' made through WLAN interface.\r\n", ssid); + CONSOLE_PRINTF(" Remote WLAN IP Address: %s.\r\n", WiFi.localIP().toString().c_str()); + } + } + } + CONSOLE_PRINTF(" Heap after NAPT init: %d\r\n", ESP.getFreeHeap()); + if (ret != ERR_OK) { + CONSOLE_PRINTF(" NAPT initialization failed!!!\r\n"); + } + + /* 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 + CONSOLE_PRINTLN("HTTP server started"); + loadCredentials(); // Load WLAN credentials from network + connect = (strlen(ssid) > 0 && staReady); // Request WLAN connect if there is a SSID and we want to connect at startup +} + +void connectWifi() { + CONSOLE_PRINTF("Connecting as wifi client, WLAN, to '%s' ...\r\n", ssid); + WiFi.disconnect(); + /* + A call to set hostname, must be set before the call to WiFi.begin, otherwise + the name may be missing from the routers DNS lookup results. Note, not all + routers will import registered DHCP host names from clients into the active + local DNS resolver. For those that do, it is best to set hostname before + calling WiFi.begin(). + */ + WiFi.hostname(myHostname); + WiFi.begin(ssid, password); + int connRes = WiFi.waitForConnectResult(); + if (-1 == connRes) { + CONSOLE_PRINTLN(" WiFi.waitForConnectResult() timed out."); + } else { + CONSOLE_PRINTF(" Connection status: %s, %d\r\n", getWiFiStatusString(connRes).c_str(), connRes); + } +} + +void loop() { + if (connect) { + connect = false; + connectWifi(); + lastConnectTry = millis(); + } + { + unsigned int s = WiFi.status(); + if (s == 0 && millis() > (lastConnectTry + 60000) && ssid[0] && staReady) { + /* When all of the following conditions are true, try to connect */ + /* 1) If WLAN disconnected */ + /* 2) Required idle time between connect attempts has passed. */ + /* 3) We have an ssid configured */ + /* 4) We are ready for the STA to come up */ + /* Don't set retry time too low as retry interfere the softAP operation */ + connect = true; + } + if (status != s) { // WLAN status change + CONSOLE_PRINTF("WLAN Status changed:\r\n"); + CONSOLE_PRINTF(" new status: %s, %d\r\n", getWiFiStatusString(s).c_str(), s); + CONSOLE_PRINTF(" previous status: %s, %d\r\n", getWiFiStatusString(status).c_str(), status); + status = s; + if (s == WL_CONNECTED) { + /* Just connected to WLAN */ + CONSOLE.println(); + if (WiFi.localIP().isSet() && WiFi.softAPIP().isSet()) { + CONSOLE_PRINTF("NAPT AP '%s' status:\r\n", softAP_ssid); + if (WiFi.localIP().isSet()) { + CONSOLE_PRINTF(" It is an extension of '%s' made through WLAN interface.\r\n", ssid); + CONSOLE_PRINTF(" WLAN connected with IP Address: %s.\r\n", WiFi.localIP().toString().c_str()); + } + } else { + CONSOLE_PRINT("WLAN connected to "); + CONSOLE.println(ssid); + CONSOLE_PRINT(" IP address: "); + CONSOLE.println(WiFi.localIP()); + } + // Setup MDNS responder + if (!MDNS.begin(myHostname, WiFi.localIP())) { + CONSOLE_PRINTLN(" Error setting up MDNS responder!"); + } else { + CONSOLE_PRINTLN(" mDNS responder started"); + // Add service to MDNS-SD + MDNS.addService("http", "tcp", 80); + } + /* + Setup the DNSServer to respond only to request for our hostname and + forward other name request to the DNS configured to the WLAN. + */ + dnsServer.setTTL(600); // 10 minutes + dnsServer.enableForwarder(myHostname, WiFi.dnsIP(0)); + CONSOLE_PRINTF("DNSServer changes/status:\r\n"); + CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off"); + CONSOLE_PRINTF(" Resolve '%s' to this AP's IP address, '%s' %s.\r\n", + dnsServer.getDomainName().c_str(), + softAP_ssid, + WiFi.softAPIP().toString().c_str()); + if (dnsServer.isDNSSet()) { + CONSOLE_PRINTF(" Forward other lookups to DNS: %s\r\n", dnsServer.getDNS().toString().c_str()); + } + CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL()); + + } else { + /* Captive portals will usually use a TTL of 0 to avoid DNS cache poisoning. */ + dnsServer.setTTL(0); + /* Setup the DNSServer to redirect all the domain lookups to the apIP */ + dnsServer.disableForwarder("*"); + CONSOLE_PRINTF("DNSServer changes/status:\r\n"); + CONSOLE_PRINTF(" DNS Forwarding is %s\r\n", dnsServer.isForwarding() ? "on" : "off"); + CONSOLE_PRINTF(" Resolve all domain lookups, '%s', to this AP's IP address, '%s' %s.\r\n", + dnsServer.getDomainName().c_str(), + softAP_ssid, + WiFi.softAPIP().toString().c_str()); + CONSOLE_PRINTF(" TTL set to %u\r\n", dnsServer.getTTL()); + + // Note, it is not necessary to clear the DNS forwarder address. This + // is being done here, to test that methods isDNSSet() and setDNS() work. + dnsServer.setDNS(0U); + if (dnsServer.isDNSSet()) { + CONSOLE_PRINTF(" DNS forwarder address: %s\r\n", dnsServer.getDNS().toString().c_str()); + } else { + CONSOLE_PRINTF(" DNS forwarder address not set.\r\n"); + } + + if (s == WL_NO_SSID_AVAIL) { + WiFi.disconnect(); + } + } + } + if (s == WL_CONNECTED) { + MDNS.update(); + } + } + // Do work: + // DNS + dnsServer.processNextRequest(); + // HTTP + server.handleClient(); +} + +#else // LWIP_FEATURES && !LWIP_IPV6 + +#include +void setup() { + WiFi.persistent(false); + WiFi.mode(WIFI_OFF); + Serial.begin(115200); + Serial.printf("\n\nNAPT not supported in this configuration\n"); +} + +void loop() { +} + +#endif // LWIP_FEATURES && !LWIP_IPV6 diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/PortalRedirectHttp.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/PortalRedirectHttp.ino new file mode 100644 index 0000000000..286490772d --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/PortalRedirectHttp.ino @@ -0,0 +1,48 @@ + +#if LWIP_FEATURES && !LWIP_IPV6 + +// Substitution list: +// {t} - target name +// {1} - The target to redirect to, in absolute URL form. +#ifdef DEBUG_VIEW +static const char portalRedirectHTML[] PROGMEM = R"EOF( + + + + + +Redirecting + + +

Captive Portal Redirect

+

Redirecting to {t}

+

If you do not see the menu in 5 seconds, please click on the above link!

+ + +)EOF"; + +#else +static const char portalRedirectHTML[] PROGMEM = R"EOF( +Redirecting

Captive Portal Redirect

Redirecting to {t}

If you do not see the menu in 5 seconds, please click on the above link!

+)EOF"; +#endif + +void sendPortalRedirect(String path, String targetName) { + CONSOLE_PRINTLN2("Request redirected to captive portal, original request was for ", server.hostHeader()); + /* There are 3 points of redirection here: + 1) "Location" element in the header + 2) HTML meta element to redirect + 3) Click on link to redirect + If the "Location" header element works the HTML stuff is never seen. + */ + // https://tools.ietf.org/html/rfc7231#section-6.4.3 + server.sendHeader("Location", path, true); + addNoCacheHeader(); + String reply = FPSTR(portalRedirectHTML); + reply.reserve(reply.length() + 2 * path.length() + 80); + reply.replace("{t}", targetName); + reply.replace("{1}", path); + server.send(302, "text/html", reply); +} + +#endif // LWIP_FEATURES && !LWIP_IPV6 diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/WifiHttp.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/WifiHttp.ino new file mode 100644 index 0000000000..f042327762 --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/WifiHttp.ino @@ -0,0 +1,325 @@ + +// #define DEBUG_VIEW +// The idea here is to debug HTML with DEBUG_VIEW defined then, when finished, +// use one of the minify web applications to strip the comments and nice +// formating spaces. Then update the minified version below. +// +// Also there are comment sections at the top and bottom of each block of HTML +// code. The purpose is to move the lines of C code near by into the blocked +// comment sections. Then you have a large block of continguious HTML that can +// be copy/pasted into one of the online web HTML checkers. You can adjust the +// code there till it is correct then copy/paste back here. Then, you can move +// comment boarders around until the C code is back in place. + +#ifdef DEBUG_VIEW +static const char configHead[] PROGMEM = R"EOF( + + + + + + +WiFi + + + + + + +)EOF"; +#else +static const char configHead[] PROGMEM = R"EOF(WiFi )EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configPresetInput[] PROGMEM = R"EOF( + + + +)EOF"; +#else +static const char configPresetInput[] PROGMEM = R"EOF()EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configConnection[] PROGMEM = R"EOF( + +
+
+

WiFi Details and Config

+

You are connected through the {w}

+ +)EOF"; +#else +static const char configConnection[] PROGMEM = R"EOF(

WiFi Details and Config

You are connected through the {w}

)EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configAPInfo[] PROGMEM = R"EOF( + +
+

SoftAP Details

+ + + + + +
SSI{s}
BSSID {b}
IP{i}
STACount: {a}
+ +)EOF"; +#else +static const char configAPInfo[] PROGMEM = R"EOF(

SoftAP Details

SSI{s}
BSSID {b}
IP{i}
STACount: {a}
)EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configWLANInfo[] PROGMEM = R"EOF( + +
+

WLAN Details

+ + + + + + + + + + + +
SSID{s}
BSSID {b}
CH{c}
PHY{p}
RSSI{r}
IP{i}
GW{g}
Mask{m}
DNS1{1}
DNS2{2}
+ +)EOF"; +#else +static const char configWLANInfo[] PROGMEM = R"EOF(

WLAN Details

SSID{s}
BSSID {b}
CH{c}
PHY{p}
RSSI{r}
IP{i}
GW{g}
Mask{m}
DNS1{1}
DNS2{2}
)EOF"; +#endif + + +#ifdef DEBUG_VIEW +static const char configWLANOffline[] PROGMEM = R"EOF( + +
+

WLAN - offline

+ +)EOF"; +#else +static const char configWLANOffline[] PROGMEM = R"EOF(

WLAN - offline

)EOF"; +#endif + + +#ifdef DEBUG_VIEW +static const char configList[] PROGMEM = R"EOF( + +
+

WLAN Network List

+

(refresh if any are missing)

+ + + + + + +)EOF"; +#else +static const char configList[] PROGMEM = R"EOF(

WLAN Network List

(refresh if any are missing)

Network Name/SSIDCHRSSI
)EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configItem[] PROGMEM = R"EOF( + + + + + + +)EOF"; +#else +static const char configItem[] PROGMEM = R"EOF()EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configNoAPs[] PROGMEM = R"EOF( + + + + + + +)EOF"; +#else +static const char configNoAPs[] PROGMEM = R"EOF()EOF"; +#endif + +#ifdef DEBUG_VIEW +static const char configEnd[] PROGMEM = R"EOF( + +
Network Name/SSIDCHRSSI
{s}{c}{l}{r}
{s}{c}{l}{r}
No WLAN found
No WLAN found
+

Connect to Network:

+ +

+ + +)EOF"; +#else +#endif + +#ifdef DEBUG_VIEW +static const char configEnd2[] PROGMEM = R"EOF( + +  👁 +

+
+
+

You may want to return to the home page.

+

+
+
+ + + +)EOF"; +#else +static const char configEnd[] PROGMEM = R"EOF(

Connect to Network:



  👁


You may want to return to the home page.

)EOF"; +#endif diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/credentials.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/credentials.ino new file mode 100644 index 0000000000..2d0827d0c2 --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/credentials.ino @@ -0,0 +1,33 @@ + +#if LWIP_FEATURES && !LWIP_IPV6 + +/** 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; + } + + CONSOLE_PRINTLN("Recovered credentials:"); + CONSOLE_PRINTF(" %s\r\n", ssid); + CONSOLE_PRINTF(" %s\r\n", strlen(password) > 0 ? "********" : ""); +} + +/** 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(); +} + +#endif // LWIP_FEATURES && !LWIP_IPV6 diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/handleHttp.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/handleHttp.ino new file mode 100644 index 0000000000..d8c2a48433 --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/handleHttp.ino @@ -0,0 +1,250 @@ +#if LWIP_FEATURES && !LWIP_IPV6 + +#ifndef TCP_MSS +#define TCP_MSS 1460 +#endif +/* + Use kMaxChunkSize to limit size of chuncks +*/ +constexpr inline size_t kMaxChunkSize = TCP_MSS; +String& sendIfOver(String& str, size_t threshold = kMaxChunkSize / 2); +size_t sendAsChunks_P(PGM_P content, size_t chunkSize = kMaxChunkSize); + +size_t maxPage = 0; + +void addNoCacheHeader() { + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); +} + + +String& sendIfOver(String& str, size_t threshold) { + size_t len = str.length(); + if (len > threshold) { + // Use later to determine if we reserved enough room in page to avoid realloc + maxPage = std::max(maxPage, len); + server.sendContent(str); + str = ""; + } + return str; +} + +/* + The idea here is to avoid a large allocation by sendContent_P to copy a + big PROGMEM string. Slice PROGMEM string into chuncks and send. +*/ +size_t sendAsChunks_P(PGM_P content, size_t chunkSize) { + size_t len = strlen_P(content); + for (size_t pos = 0; pos < len; pos += chunkSize) { + server.sendContent_P(&content[pos], ((len - pos) >= chunkSize) ? chunkSize : len - pos); + } + return len; +} + +/** Handle root or redirect to captive portal */ +void handleRoot() { + if (captivePortal()) { + // If captive portal is needed, redirect instead of displaying the page. + return; + } + addNoCacheHeader(); + + String Page; + Page += F( + "" + "" + "ADV CAP Portal Example" + "" + "

HELLO WORLD!!

"); + if (server.client().localIP() == apIP) { + Page += String(F("

You are connected through the soft AP: ")) + softAP_ssid + F("

"); + } else { + Page += String(F("

You are connected through the wifi network: ")) + ssid + F("

"); + } + Page += F( + "

You may want to config the wifi connection.

" + ""); + + server.send(200, F("text/html"), Page); +} + +/* + Redirect to the captive portal if we got a request for another domain. + Return true in that case, so the page handler does not try to handle + the request again. +*/ +boolean captivePortal() { + IPAddress hAddr, cAddr; + + cAddr = server.client().localIP(); + if (!cAddr.isSet()) { + // The connection closed prematurely on us. + // Return true, so no further action is taken. + return true; + } + + if (hAddr.fromString(server.hostHeader()) && hAddr == cAddr) { + return false; + } + + if (hAddr.isSet() || (server.hostHeader() != (String(myHostname) + ".local") && // arrived here by mDNS + server.hostHeader() != String(myHostname))) { // arrived here by local router DNS + String whereTo = String("http://") + server.client().localIP().toString(); + sendPortalRedirect(whereTo, F("Captive Portal Example")); + return true; + } + + return false; +} + + +/** Wifi Details and Config page handler */ +void handleWifi() { + addNoCacheHeader(); + + // use HTTP/1.1 Chunked response to avoid building a huge temporary string + if (!server.chunkedResponseModeStart(200, F("text/html"))) { + server.send(505, F("text/plain"), F("HTTP1.1 required")); + return; + } + + // Send a few chunks of the HTML that don't need to change. + sendAsChunks_P(configHead); + + String page; + + CONSOLE_PRINTLN2("sizeof(configHead): ", (sizeof(configHead))); + CONSOLE_PRINTLN2("sizeof(configWLANInfo): ", (sizeof(configWLANInfo))); + CONSOLE_PRINTLN2("sizeof(configList): ", (sizeof(configList))); + CONSOLE_PRINTLN2("sizeof(configEnd): ", (sizeof(configEnd))); + // Just do max on some of the visually larger HTML chunks that will be loaded + // into page and add a little for growth when substituting values in. + size_t thisMany = std::max(sizeof(configWLANInfo), sizeof(configList)) + 200; + CONSOLE_PRINTLN2("Estimate Minimum page reserve size: ", (thisMany)); + page.reserve(std::max(kMaxChunkSize, thisMany)); + + page = FPSTR(configPresetInput); + /* + Set previously used/entered credentials as a default entries. + This allows an opportunity to correct them and try again. + */ + page.replace("{s}", String(ssid)); + page.replace("{p}", String(password)); + sendIfOver(page); + + page += FPSTR(configConnection); + if (server.client().localIP() == apIP) { + page.replace("{w}", String(F("SoftAP: ")) + softAP_ssid); + } else { + page.replace("{w}", String(F("WiFi Network: ")) + ssid); + } + + /* + To avoid sending lots of small packets. We call this function frequently, + to check if the 'page' has gone over 512 bytes and if so send. + */ + sendIfOver(page); + + page += FPSTR(configAPInfo); + { + uint8_t sta_cnt = wifi_softap_get_station_num(); + page.replace("{s}", String(softAP_ssid)); + page.replace("{b}", String(WiFi.softAPmacAddress())); + page.replace("{i}", WiFi.softAPIP().toString()); + page.replace("{a}", String(sta_cnt)); + sendIfOver(page); + if (sta_cnt) { + page += String(F("\r\n
\r\n"));
+      struct station_info* info = wifi_softap_get_station_info();
+      IPAddress addr;
+      while (info != NULL) {
+        addr = info->ip;
+        page += macToString(info->bssid) + F("  ") + addr.toString() + F("\r\n");
+        info = STAILQ_NEXT(info, next);
+        sendIfOver(page);
+      }
+      page += F("
\r\n"); + } + } + + /* + Before we prepare a large block for sending, we call 'sendIfOver' with a + threshold of 0 to force the sending of the current 'page' content. + */ + + if (WiFi.localIP().isSet()) { + sendIfOver(page, 0); + page += FPSTR(configWLANInfo); + page.replace("{s}", String(ssid)); + page.replace("{b}", macToString(bssid)); + page.replace("{c}", String(WiFi.channel())); + page.replace("{p}", String(F("802.11")) + (getPhyModeChar(WiFi.getPhyMode()))); + page.replace("{r}", String(WiFi.RSSI())); + page.replace("{i}", WiFi.localIP().toString()); + page.replace("{g}", WiFi.gatewayIP().toString()); + page.replace("{m}", WiFi.subnetMask().toString()); + page.replace("{1}", WiFi.dnsIP(0).toString()); + page.replace("{2}", WiFi.dnsIP(1).toString()); + } else { + page += FPSTR(configWLANOffline); + } + + sendIfOver(page, 0); + sendAsChunks_P(configList); + + CONSOLE_PRINTLN("scan start"); + int n = WiFi.scanNetworks(); + CONSOLE_PRINTLN("scan done"); + + if (n > 0) { + for (size_t i = 0; i < (size_t)n; i++) { + page += FPSTR(configItem); + page.replace("{s}", WiFi.SSID(i)); + page.replace("{t}", WiFi.BSSIDstr(i)); + page.replace("{c}", String(WiFi.channel(i))); + page.replace("{l}", (WiFi.encryptionType(i) == ENC_TYPE_NONE) ? F("") : F("🔒")); + page.replace("{r}", String(WiFi.RSSI(i))); + sendIfOver(page); + } + } else { + page += FPSTR(configNoAPs); + } + sendIfOver(page, 0); // send what we have buffered before next direct send. + sendAsChunks_P(configEnd); + + CONSOLE_PRINTLN2("MAX String memory used: ", (maxPage)); + server.chunkedResponseFinalize(); +} + +/** Handle the WLAN save form and redirect to WLAN config page again */ +void handleWifiSave() { + CONSOLE_PRINTLN("wifi save"); + server.arg("n").toCharArray(ssid, sizeof(ssid) - 1); + server.arg("p").toCharArray(password, sizeof(password) - 1); + sendPortalRedirect(F("wifi"), F("Wifi Config")); + saveCredentials(); + connect = strlen(ssid) > 0; // Request WLAN connect with new credentials if there is a SSID +} + +void handleNotFound() { + if (captivePortal()) { // If captive portal redirect instead of displaying the error page. + return; + } + String message = F("File Not Found\r\n\r\n"); + message += F("URI: "); + message += server.uri(); + message += F("\r\nMethod: "); + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += F("\r\nArguments: "); + message += server.args(); + message += F("\r\n"); + + for (uint8_t i = 0; i < server.args(); i++) { + message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\r\n"); + } + addNoCacheHeader(); + server.send(404, F("text/plain"), message); +} + +#endif // LWIP_FEATURES && !LWIP_IPV6 diff --git a/libraries/DNSServer/examples/NAPTCaptivePortal/tools.ino b/libraries/DNSServer/examples/NAPTCaptivePortal/tools.ino new file mode 100644 index 0000000000..976ecc9fb4 --- /dev/null +++ b/libraries/DNSServer/examples/NAPTCaptivePortal/tools.ino @@ -0,0 +1,84 @@ +/* + These functions may exist in other projects +*/ + +#if LWIP_FEATURES && !LWIP_IPV6 + +/* + Returns a descriptive string for WiFi.status() value +*/ +String getWiFiStatusString(uint32_t status) { + const __FlashStringHelper* r; + switch (status) { + case WL_IDLE_STATUS: + r = F("WL_IDLE_STATUS"); + break; + + case WL_NO_SSID_AVAIL: + r = F("WL_NO_SSID_AVAIL"); + break; + + case WL_SCAN_COMPLETED: + r = F("WL_SCAN_COMPLETED"); + break; + + case WL_CONNECTED: + r = F("WL_CONNECTED"); + break; + + case WL_CONNECT_FAILED: + r = F("WL_CONNECT_FAILED"); + break; + + case WL_CONNECTION_LOST: + r = F("WL_CONNECTION_LOST"); + break; + + case WL_DISCONNECTED: + r = F("WL_DISCONNECTED"); + break; + + case WL_NO_SHIELD: + r = F("WL_NO_SHIELD"); + break; + + default: + return String(F("Unknown: 0x")) + String(status, HEX); + } + return String(r); +} + +/* + Returns a single charcter to append to a "802.11" string to describe the PHY + mode of a WiFi device. Can be used with the value returned by WiFi.getPhyMode(). +*/ +char getPhyModeChar(WiFiPhyMode_t i) { + switch (i) { + case WIFI_PHY_MODE_11B: + return 'b'; // = 1 + case WIFI_PHY_MODE_11G: + return 'g'; // = 2, + case WIFI_PHY_MODE_11N: + return 'n'; // = 3, + default: + break; + } + return '?'; +} + +/* + Return a String of 6 colon separated hex bytes. + This format is commonly used when printing 6 byte MAC addresses. +*/ +String macToString(const unsigned char* mac) { + char buf[20]; + int rc = snprintf(buf, sizeof(buf), PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + if (rc < 0 || rc >= (int)sizeof(buf)) { + return emptyString; + } + return String(buf); +} + +#endif // LWIP_FEATURES && !LWIP_IPV6 diff --git a/libraries/DNSServer/src/DNSServer.cpp b/libraries/DNSServer/src/DNSServer.cpp index 8d7b3dfb06..d2c41f79d7 100644 --- a/libraries/DNSServer/src/DNSServer.cpp +++ b/libraries/DNSServer/src/DNSServer.cpp @@ -1,33 +1,139 @@ +#include #include "DNSServer.h" #include #include #include +extern struct rst_info resetInfo; #ifdef DEBUG_ESP_PORT -#define DEBUG_OUTPUT DEBUG_ESP_PORT +#define CONSOLE DEBUG_ESP_PORT #else -#define DEBUG_OUTPUT Serial +#define CONSOLE Serial +#endif + +#define _ETS_PRINTF(a, ...) ets_uart_printf(a, ##__VA_ARGS__) +#define _ETS_PRINTFNL(a, ...) ets_uart_printf(a "\n", ##__VA_ARGS__) +#define _PRINTF(a, ...) printf_P(PSTR(a), ##__VA_ARGS__) +#define _PRINT(a) print(String(F(a))) +#define _PRINTLN(a) println(String(F(a))) +#define _PRINTLN2(a, b) println(String(F(a)) + b ) + +#define ETS_PRINTF _ETS_PRINTF +#define ETS_PRINTFNL _ETS_PRINTFNL +#define CONSOLE_PRINTF CONSOLE._PRINTF +#define CONSOLE_PRINT CONSOLE._PRINT +#define CONSOLE_PRINTLN CONSOLE._PRINTLN +#define CONSOLE_PRINTLN2 CONSOLE._PRINTLN2 + + +#ifdef DEBUG_DNSSERVER +#define DEBUG_PRINTF CONSOLE_PRINTF +#define DEBUG_PRINT CONSOLE_PRINT +#define DEBUG_PRINTLN CONSOLE_PRINTLN +#define DEBUG_PRINTLN2 CONSOLE_PRINTLN2 +#define DBGLOG_FAIL LOG_FAIL + +#define DEBUG_(...) do { (__VA_ARGS__); } while(false) +#define DEBUG__(...) __VA_ARGS__ +#define LOG_FAIL(a, fmt, ...) do { if (!(a)) { CONSOLE.printf_P( PSTR(fmt " line: %d, function: %s\r\n"), ##__VA_ARGS__, __LINE__, __FUNCTION__ ); } } while(false); + +#else +#define DEBUG_PRINTF(...) do { } while(false) +#define DEBUG_PRINT(...) do { } while(false) +#define DEBUG_PRINTLN(...) do { } while(false) +#define DEBUG_PRINTLN2(...) do { } while(false) +#define DEBUG_(...) do { } while(false) +#define DEBUG__(...) do { } while(false) +#define LOG_FAIL(a, ...) do { a; } while(false) +#define DBGLOG_FAIL(...) do { } while(false) #endif #define DNS_HEADER_SIZE sizeof(DNSHeader) +// Want to keep IDs unique across restarts and continquious +static uint32_t _ids __attribute__((section(".noinit"))); + DNSServer::DNSServer() { + // I have observed that using 0 for captive and non-zero (600) when + // forwarding, will help Android devices recognize the change in connectivity. + // They will then report connected. _ttl = lwip_htonl(60); + + if (REASON_DEFAULT_RST == resetInfo.reason || + REASON_DEEP_SLEEP_AWAKE <= resetInfo.reason) { + _ids = random(0, BIT(16) - 1); + } + _ids += kDNSSQueSize; // for the case of restart, ignore any inflight responses + _errorReplyCode = DNSReplyCode::NonExistentDomain; } +void DNSServer::disableForwarder(const String &domainName, bool freeResources) +{ + _forwarder = false; + if (!domainName.isEmpty()) { + _domainName = domainName; + downcaseAndRemoveWwwPrefix(_domainName); + } + if (freeResources) { + _dns = (uint32_t)0; + if (_que) { + _que = nullptr; + DEBUG_PRINTF("from stop, deleted _que\r\n"); + DEBUG_(({ + if (_que_ov) { + DEBUG_PRINTLN2("DNS forwarder que overflow or no reply to request: ", (_que_ov)); + } + if (_que_drop) { + DEBUG_PRINTLN2("DNS forwarder que wrapped, reply dropped: ", (_que_drop)); + } + })); + } + } +} + +bool DNSServer::enableForwarder(const String &domainName, const IPAddress &dns) +{ + disableForwarder(domainName, false); // Just happens to have the same logic needed here. + + if (dns.isSet()) { + _dns = dns; + } + + if (_dns.isSet()) { + if (!_que) { + _que = std::unique_ptr (new (std::nothrow) DNSS_REQUESTER[kDNSSQueSize]); + DEBUG_PRINTF("Created new _que\r\n"); + if (_que) { + for (size_t i = 0; i < kDNSSQueSize; i++) { + _que[i].ip = 0; + } + DEBUG_((_que_ov = 0)); + DEBUG_((_que_drop = 0)); + } + } + if (_que) { + _forwarder = true; + } + } + return _forwarder; +} + bool DNSServer::start(const uint16_t &port, const String &domainName, - const IPAddress &resolvedIP) + const IPAddress &resolvedIP, const IPAddress &dns) { - _port = port; - - _domainName = domainName; + _port = (port) ? port : IANA_DNS_PORT; + _resolvedIP[0] = resolvedIP[0]; _resolvedIP[1] = resolvedIP[1]; _resolvedIP[2] = resolvedIP[2]; _resolvedIP[3] = resolvedIP[3]; - downcaseAndRemoveWwwPrefix(_domainName); + + if (!enableForwarder(domainName, dns) && (dns.isSet() || _dns.isSet())) { + return false; + } + return _udp.begin(_port) == 1; } @@ -41,9 +147,15 @@ void DNSServer::setTTL(const uint32_t &ttl) _ttl = lwip_htonl(ttl); } +uint32_t DNSServer::getTTL() +{ + return lwip_ntohl(_ttl); +} + void DNSServer::stop() { _udp.stop(); + disableForwarder(emptyString, true); } void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName) @@ -53,7 +165,58 @@ void DNSServer::downcaseAndRemoveWwwPrefix(String &domainName) domainName.remove(0, 4); } -void DNSServer::respondToRequest(uint8_t *buffer, size_t length) +void DNSServer::forwardReply(uint8_t *buffer, size_t length) +{ + if (!_forwarder || !_que) { + return; + } + DNSHeader *dnsHeader = (DNSHeader *)buffer; + uint16_t id = dnsHeader->ID; + // if (kDNSSQueSize <= (uint16_t)((uint16_t)_ids - id)) { + if ((uint16_t)kDNSSQueSize <= (uint16_t)_ids - id) { + DEBUG_((++_que_drop)); + DEBUG_PRINTLN2("Forward reply ID: 0x", (String(id, HEX) + F(" dropped!"))); + return; + } + size_t i = id & (kDNSSQueSize - 1); + + // Drop duplicate packets + if (0 == _que[i].ip) { + DEBUG_PRINTLN2("Duplicate reply dropped ID: 0x", String(id, HEX)); + return; + } + dnsHeader->ID = _que[i].id; + _udp.beginPacket(_que[i].ip, _que[i].port); + _udp.write(buffer, length); + _udp.endPacket(); + DEBUG_PRINTLN2("Forward reply ID: 0x", (String(id, HEX) + F(" to ") + IPAddress(_que[i].ip).toString())); + _que[i].ip = 0; // This gets used to detect duplicate packets and overflow +} + +void DNSServer::forwardRequest(uint8_t *buffer, size_t length) +{ + if (!_forwarder || !_dns.isSet() || !_que) { + return; + } + DNSHeader *dnsHeader = (DNSHeader *)buffer; + ++_ids; + size_t i = _ids & (kDNSSQueSize - 1); + DEBUG_(({ + if (0 != _que[i].ip) { + ++_que_ov; + } + })); + _que[i].ip = _udp.remoteIP(); + _que[i].port = _udp.remotePort(); + _que[i].id = dnsHeader->ID; + dnsHeader->ID = (uint16_t)_ids; + _udp.beginPacket(_dns, IANA_DNS_PORT); + _udp.write(buffer, length); + _udp.endPacket(); + DEBUG_PRINTLN2("Forward request ID: 0x", (String(dnsHeader->ID, HEX) + F(" to ") + _dns.toString())); +} + +bool DNSServer::respondToRequest(uint8_t *buffer, size_t length) { DNSHeader *dnsHeader; uint8_t *query, *start; @@ -64,23 +227,30 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length) dnsHeader = (DNSHeader *)buffer; // Must be a query for us to do anything with it - if (dnsHeader->QR != DNS_QR_QUERY) - return; + if (dnsHeader->QR != DNS_QR_QUERY) { + return false; + } // If operation is anything other than query, we don't do it - if (dnsHeader->OPCode != DNS_OPCODE_QUERY) - return replyWithError(dnsHeader, DNSReplyCode::NotImplemented); + if (dnsHeader->OPCode != DNS_OPCODE_QUERY) { + replyWithError(dnsHeader, DNSReplyCode::NotImplemented); + return false; + } // Only support requests containing single queries - everything else // is badly defined - if (dnsHeader->QDCount != lwip_htons(1)) - return replyWithError(dnsHeader, DNSReplyCode::FormError); + if (dnsHeader->QDCount != lwip_htons(1)) { + replyWithError(dnsHeader, DNSReplyCode::FormError); + return false; + } // We must return a FormError in the case of a non-zero ARCount to // be minimally compatible with EDNS resolvers if (dnsHeader->ANCount != 0 || dnsHeader->NSCount != 0 - || dnsHeader->ARCount != 0) - return replyWithError(dnsHeader, DNSReplyCode::FormError); + || dnsHeader->ARCount != 0) { + replyWithError(dnsHeader, DNSReplyCode::FormError); + return false; + } // Even if we're not going to use the query, we need to parse it // so we can check the address type that's being queried @@ -89,15 +259,19 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length) remaining = length - DNS_HEADER_SIZE; while (remaining != 0 && *start != 0) { labelLength = *start; - if (labelLength + 1 > remaining) - return replyWithError(dnsHeader, DNSReplyCode::FormError); + if (labelLength + 1 > remaining) { + replyWithError(dnsHeader, DNSReplyCode::FormError); + return false; + } remaining -= (labelLength + 1); start += (labelLength + 1); } // 1 octet labelLength, 2 octet qtype, 2 octet qclass - if (remaining < 5) - return replyWithError(dnsHeader, DNSReplyCode::FormError); + if (remaining < 5) { + replyWithError(dnsHeader, DNSReplyCode::FormError); + return false; + } start += 1; // Skip the 0 length label that we found above @@ -109,23 +283,33 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length) queryLength = start - query; if (qclass != lwip_htons(DNS_QCLASS_ANY) - && qclass != lwip_htons(DNS_QCLASS_IN)) - return replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, - query, queryLength); + && qclass != lwip_htons(DNS_QCLASS_IN)) { + replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, query, queryLength); + return false; + } if (qtype != lwip_htons(DNS_QTYPE_A) - && qtype != lwip_htons(DNS_QTYPE_ANY)) - return replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, - query, queryLength); + && qtype != lwip_htons(DNS_QTYPE_ANY)) { + replyWithError(dnsHeader, DNSReplyCode::NonExistentDomain, query, queryLength); + return false; + } // If we have no domain name configured, just return an error - if (_domainName.isEmpty()) - return replyWithError(dnsHeader, _errorReplyCode, - query, queryLength); + if (_domainName.isEmpty()) { + if (_forwarder) { + return true; + } else { + replyWithError(dnsHeader, _errorReplyCode, query, queryLength); + return false; + } + } // If we're running with a wildcard we can just return a result now - if (_domainName == "*") - return replyWithIP(dnsHeader, query, queryLength); + if (_domainName == "*") { + DEBUG_PRINTF("dnsServer - replyWithIP\r\n"); + replyWithIP(dnsHeader, query, queryLength); + return false; + } matchString = _domainName.c_str(); @@ -139,24 +323,32 @@ void DNSServer::respondToRequest(uint8_t *buffer, size_t length) labelLength = *start; start += 1; while (labelLength > 0) { - if (tolower(*start) != *matchString) - return replyWithError(dnsHeader, _errorReplyCode, - query, queryLength); + if (tolower(*start) != *matchString) { + if (_forwarder) { + return true; + } else { + replyWithError(dnsHeader, _errorReplyCode, query, queryLength); + return false; + } + } ++start; ++matchString; --labelLength; } - if (*start == 0 && *matchString == '\0') - return replyWithIP(dnsHeader, query, queryLength); + if (*start == 0 && *matchString == '\0') { + replyWithIP(dnsHeader, query, queryLength); + return false; + } - if (*matchString != '.') - return replyWithError(dnsHeader, _errorReplyCode, - query, queryLength); + if (*matchString != '.') { + replyWithError(dnsHeader, _errorReplyCode, query, queryLength); + return false; + } ++matchString; } - return replyWithError(dnsHeader, _errorReplyCode, - query, queryLength); + replyWithError(dnsHeader, _errorReplyCode, query, queryLength); + return false; } void DNSServer::processNextRequest() @@ -182,7 +374,14 @@ void DNSServer::processNextRequest() return; _udp.read(buffer.get(), currentPacketSize); - respondToRequest(buffer.get(), currentPacketSize); + if (_dns.isSet() && _udp.remoteIP() == _dns) { + // _forwarder may have been set to false; however, for now allow inflight + // replys to finish. //?? + forwardReply(buffer.get(), currentPacketSize); + } else + if (respondToRequest(buffer.get(), currentPacketSize)) { + forwardRequest(buffer.get(), currentPacketSize); + } } void DNSServer::writeNBOShort(uint16_t value) diff --git a/libraries/DNSServer/src/DNSServer.h b/libraries/DNSServer/src/DNSServer.h index ad50516c81..1ca16697f9 100644 --- a/libraries/DNSServer/src/DNSServer.h +++ b/libraries/DNSServer/src/DNSServer.h @@ -2,6 +2,14 @@ #define DNSServer_h #include +// #define DEBUG_DNSSERVER + +// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt +#ifndef IANA_DNS_PORT +#define IANA_DNS_PORT 53 // AKA domain +constexpr inline uint16_t kIanaDnsPort = IANA_DNS_PORT; +#endif + #define DNS_QR_QUERY 0 #define DNS_QR_RESPONSE 1 #define DNS_OPCODE_QUERY 0 @@ -45,6 +53,15 @@ struct DNSHeader uint16_t ARCount; // number of resource entries }; +constexpr inline size_t kDNSSQueSizeAddrBits = 3; // The number of bits used to address que entries +constexpr inline size_t kDNSSQueSize = BIT(kDNSSQueSizeAddrBits); + +struct DNSS_REQUESTER { + uint32_t ip; + uint16_t port; + uint16_t id; +}; + class DNSServer { public: @@ -52,24 +69,60 @@ class DNSServer ~DNSServer() { stop(); }; + /* + If specified, `enableForwarder` will update the `domainName` that is used + to match DNS request to this AP's IP Address. A non-matching request will + be forwarded to the DNS server specified by `dns`. + + Returns `true` on success. + + Returns `false`, + * when forwarding `dns` is not set, or + * unable to allocate resources for managing the DNS forward function. + */ + bool enableForwarder(const String &domainName = emptyString, const IPAddress &dns = (uint32_t)0); + /* + `disableForwarder` will stop forwarding DNS requests. If specified, + updates the `domainName` that is matched for returning this AP's IP Address. + Optionally, resources used for the DNS forward function can be freed. + */ + void disableForwarder(const String &domainName = emptyString, bool freeResources = false); + bool isForwarding() { return _forwarder && _dns.isSet(); } + void setDNS(const IPAddress& dns) { _dns = dns; } + IPAddress getDNS() { return _dns; } + bool isDNSSet() { return _dns.isSet(); } + void processNextRequest(); void setErrorReplyCode(const DNSReplyCode &replyCode); void setTTL(const uint32_t &ttl); + uint32_t getTTL(); + String getDomainName() { return _domainName; } // Returns true if successful, false if there are no sockets available bool start(const uint16_t &port, const String &domainName, - const IPAddress &resolvedIP); + const IPAddress &resolvedIP, + const IPAddress &dns = (uint32_t)0); // stops the DNS server void stop(); private: WiFiUDP _udp; - uint16_t _port; String _domainName; - unsigned char _resolvedIP[4]; + IPAddress _dns; + std::unique_ptr _que; uint32_t _ttl; +#ifdef DEBUG_DNSSERVER + // There are 2 possiblities for OverFlow: + // 1) we have more than kDNSSQueSize request already outstanding. + // 2) we have request that never received a reply. + uint32_t _que_ov; + uint32_t _que_drop; +#endif DNSReplyCode _errorReplyCode; + bool _forwarder; + unsigned char _resolvedIP[4]; + uint16_t _port; void downcaseAndRemoveWwwPrefix(String &domainName); void replyWithIP(DNSHeader *dnsHeader, @@ -81,7 +134,9 @@ class DNSServer size_t queryLength); void replyWithError(DNSHeader *dnsHeader, DNSReplyCode rcode); - void respondToRequest(uint8_t *buffer, size_t length); + bool respondToRequest(uint8_t *buffer, size_t length); + void forwardRequest(uint8_t *buffer, size_t length); + void forwardReply(uint8_t *buffer, size_t length); void writeNBOShort(uint16_t value); }; #endif