Skip to content

Added a DNSServer library #396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2015
Merged

Conversation

knovoselic
Copy link
Contributor

Hey guys,

I wanted to be able to connect to WiFi created by ESP8266 (softAP mode), enter a domain name in my browser (e.g. www.example.com) and have it hit webserver on ESP8266. I've also considered using mDNS library, but it doesn't work in AP mode.

So I decided to write a simple DNS server library. I've been testing it today and it's working fine, so I'm creating this pull request. Let me know what do you think and if you'd like to include it in your repo.

@Duality4Y
Copy link
Contributor

I believe you can do the same thing with ESP8266mDNS, there is a example in the ide that works for me.

@knovoselic
Copy link
Contributor Author

Hey @Duality4Y, as far as I know, ESP8266mDNS has two cons over this lib:
-it doesn't work in softAP mode (maybe that's fixed?)
-you need mDNS client running on your PC (Mac has it by default, Linux/Windows users need to install it manually)

This lib on the other hand works in softAP mode and doesn't need additional software on a client because it's acting as a real DNS server.

@Duality4Y
Copy link
Contributor

I see, i didn't have to install anything under Linux though it just worked. but our network has good dhcp/dns support :)

and i don't know about that softAP, I'll give it a try.

indeed the readme say that it only works in STA mode :)

@knovoselic
Copy link
Contributor Author

I guess you were lucky and already had mDNS client installed on your machine. That would be great, please let me know if it's working.

igrr added a commit that referenced this pull request Jun 11, 2015
@igrr igrr merged commit 297df29 into esp8266:esp8266-sdk-1.0 Jun 11, 2015
@TimeTravelingOwls
Copy link

It's working for me, and it's quite handy to have a DNS server running in softAP mode - thanks!

I have a suggestion and a request, which I think will make your library easier to implement and more useful.

First, although your example compiles and runs without a hitch, I had to add a few lines of code to "see" that it was working. Adding a server and minimal output makes it immediately apparent that DNS is being redirected, without having to squint:

#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>

const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 1, 1);
DNSServer dnsServer;
ESP8266WebServer server(80);

void setup() {
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
WiFi.softAP("DNSServer example");

// modify TTL associated with the domain name (in seconds)
// default is 60 seconds
dnsServer.setTTL(300);
// 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
dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure);
//start DNS server for a specific domain name
dnsServer.start(DNS_PORT, "www.example.com", apIP);

server.on("/", handleRoot);
server.begin();
}

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

void handleRoot() {
server.send(200, "text/html", "

Redirecting you to ESP8266

");
}

[I'm having noob trouble posting code, but I just hacked in a few lines from Majenko's WiFiAccessPoint example - he deserves any credit for the code.]

Second, I would like you to consider adding a captivePortal function to the library, which redirects all requests (not just specified URLs). I was able to get most of the way there by modifying processNextRequest() to remove the getDomainNameWithoutWwwPrefix() == _domainName comparison, but that's an ugly approach and doesn't handle requests with subdirs (e.g., www.example.com/example).

captivePortal allows a non-technical user to be automatically connected to the ESP8266's webserver, without having to type a specific address in their browser. It's one of the missing pieces in auto-configuration routines, and a practical requirement for implementing WiFi broadcast beacons.

Thanks again for this library, and I hope you're enjoying the weekend.

@knovoselic
Copy link
Contributor Author

@TimeTravelingOwls Hey, thanks for the ideas. I won't have any time to work on this 'till the weekend, so I'll let you know then what I'll be able to do.

@knovoselic
Copy link
Contributor Author

@TimeTravelingOwls Thanks for the suggestions. I've improved DNSServer example with your code (I was using dig to test it so I just assumed everyone else would do the same :)). I've also added captive portal example - please check it out :)
Everything is in PR #448.

@TimeTravelingOwls
Copy link

Fantastic! Is there a repo where I can download it before it has been merged?

(Also, what is dig? Just a digital comparison, or a yet another tool I don't know about?)

@knovoselic
Copy link
Contributor Author

@TimeTravelingOwls
Copy link

Thanks - I could've sworn I looked there earlier and didn't see the new code. I've got a horde of very loud 11-year olds playing D&D in front of my computer at the moment, but i'll load it up tonight and test.

Thanks for the dig link, too - looks super handy.

@knovoselic
Copy link
Contributor Author

No problem, glad I could help :) Let me know if you find any bugs or have any other ideas.

@TimeTravelingOwls
Copy link

Not sure whether this is a bug or the way you intended it: both the CaptivePortal sketch and the DNSServer sketch (when "*" is specified in dnsServer.start) trip Apple's CNA, which pops up the familiar Login screen on IOS devices. This might be desirable if you're trying to create an authorization page, for example, but it's a roadblock for a simple configuration routine, where you want any browser to display the config menu.

This didn't happen with the previous example code, and still doesn't when I compile that code with this Ver 1.1 library. The new code does do a nice job of stripping the URI.

The main difference between the two seems to be the server code - the example in my previous message uses the vanilla server routine:
server.on("/", handleRoot);
while your new code uses:
webServer.onNotFound([]() {
which is where you're stripping the URI, right? Should there be some other kind of init before the onNotFound?

If this is the way you intended it to work, may I suggest a switch that allows you to bypass the CNA?

Don't mean to sound critical - I like the choice to make the redirect global with a "*", and your demo code is quite polished. I'll have another look in the morning and see if I can get dig working on the iPad (there's free IOS version!), which may be helpful.

@TimeTravelingOwls
Copy link

Confirmed that stripping out the onNotFound routine makes the CNA go away:

webServer.on("/", handleRoot);
webServer.begin();

//  webServer.onNotFound([]() {
//    String message = "Hello World!\n\n";
//    message += "URI: ";
//    message += webServer.uri();
//
//    webServer.send(200, "text/plain", message);
//  });
//  webServer.begin();
}

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

void handleRoot() {
  webServer.send(200, "text/html", "<h1>Redirecting you to ESP8266</h1>");
}

This approach allows the IOS devices to redirect in the browser, without the popup page, but it also still chokes on the URIs (which makes sense, because it's no longer processing them).

@TimeTravelingOwls
Copy link

"Success" is the key to success!

Here's what I've learned: to determine whether it's connected to the internet, Apple's CNA issues a GET to one of a pool of sites (e.g., www.apple.com - there are nine or ten sites in the pool) and looks for success.html to be returned. But all it cares about is finding the string "success" in the response. So changing the title in responseHTML to "Success" defeats the CNA:

<!DOCTYPE html><html><head><title>Success</title></head><body>

It's not an elegant solution, but points me in the right direction. I still don't really understand why webServer.on("/", handleRoot); doesn't trigger the CNA, but I'm learning (though much more slowly than i'd like).

One other thing: https URLs aren't redirected. That doesn't seem like a big deal, but it's confusing when you hit refresh and it doesn't work. Mobile browsers typically don't display the URL prefix, and many sites seem to default to https these days. Is it possible to redirect https to http?

I'll work on an example that allows a switch for the CNA and an ack page that will clear the CNA on user input and continue to a browser.

I hope this is helpful - I really appreciate the work you've done here!

@igrr
Copy link
Member

igrr commented Jun 21, 2015

Would also be nice to have this somewhat working with captive portal check as implemented in Android:
http://stackoverflow.com/a/14030276.

@TimeTravelingOwls
Copy link

Working on it - there's a thread here: http://www.esp8266.com/viewtopic.php?f=32&t=3618

@knovoselic
Copy link
Contributor Author

@TimeTravelingOwls I think from DNSServer side everything needed is fully implemented. You just have to tweak ESP8266WebServer to behave as you'd expect it. I saw that it triggered Apple's CNA, but didn't have any more time to research it.
webServer.on("/", handleRoot); doesn't trigger the CNA because it only responds when client tries to access site without path. For example, it handles this URL: www.example.com, but not this URL: www.example.com/test. For the/test URL 404 would be returned. I've added onNotFound to make sure that all requests are replied by the same URL, regardless of path.

Because Apple opens the following URL when checking for captive portal:
http://www.apple.com/library/test/success.html
You can try something like this:
webServer.on("/library/test/success.html", handleApple);

And in handleApple return the HTML that they expect. And leave onNotFound implementation from the example to handle all other requests. That way Apple will think there's no captive portal, and all other requests will return HTML from onNotFound.

As for HTTPS, it won't be easy. Firstly, HTTPS works on port 443 (where http works on port 80). That's why it isn't caught by the example sketch. But even if you create another instance of WebServer, it probably won't work as you'd need to handle SSL requests (with a valid SSL certificate). And as far as I know, there's not library for ESP that does that. Using self-signed SSL certificate wouldn't work, because each user would see something like this:
http://cdn.inmotionhosting.com/support/images/stories/website/errors/ssl/firefox-self-signed-ssl-warning.png

@TimeTravelingOwls
Copy link

OK, I understand your rationale for setting it up to have onNotFound collect anything that hasn't been sifted out. But I'm struggling with CNA.

Because Apple opens the following URL when checking for captive portal: http://www.apple.com/library/test/success.html You can try something like this: webServer.on("/library/test/success.html", handleApple);

That changed in IOS7 - Apple now uses a domain chosen from a pool and a random URI, like this:Domain: thinkdifferent.us/TgvdkOZ1/BHVTQoca/Q9REJ2Z2.html It seems to be a reasonably small pool (fewer than a dozen), but they can obviously change it at any time, and IOS9 may be completely different. "Whitelisting" these domains for special treatment seems like a bad idea - we end up playing whack-a-mole with each change.

I'll look for a way to detect that we've triggered the CNA, so that I can offer a button-press pass-through.

As for HTTPS, the situation reminds me of the old joke:

Patient (bending his elbow backwards): "Doctor, it hurts when I do this!"
Doctor: "So don't do that!"

Have you tested your examples on Android? There's a guy on the community site who's having trouble getting it to work, though it works fine for him on IOS.

Thanks for your patience with this - I'm hoping to find time tomorrow to learn how to detect the CNA and put together a sketch that will handle it.

@knovoselic
Copy link
Contributor Author

@TimeTravelingOwls Any luck with Apple CNA? As for android, I'll check it out when I get some free time, if not sooner then in two weeks.

@marciodel
Copy link

Have you tested the library with Windows? It seems that Windows is not reconizing the answer. The query is arriving at ESP8266, but ping and browsers on Windows does not work.

@marciodel
Copy link

Just tested with Android. It is not working. I am trying to use it with 1.6.4-673-g8cd3697 release. Do you know if it works with it?

@TimeTravelingOwls
Copy link

@marciodel - Check this thread on the community site: http://www.esp8266.com/viewtopic.php?f=32&t=3618&hilit=dnsserver&start=40 Android and Windows work with a minor mod to the library:

void DNSServer::replyWithIP()
{
  _dnsHeader->QR = DNS_QR_RESPONSE;
  _dnsHeader->ANCount = _dnsHeader->QDCount;
//  _dnsHeader->QDCount = 0;
  _dnsHeader->RA = 1;                                      //added

  _udp.beginPacket(_udp.remoteIP(), _udp.remotePort());
  _udp.write(_buffer, _currentPacketSize);

  _udp.write((uint8_t)192);     //  answer name is a pointer
   _udp.write((uint8_t)12);      // pointer to offset at 0x00c

      _udp.write((uint8_t)0);   // 0x0001  answer is type A query (host address)
  _udp.write((uint8_t)1);

    _udp.write((uint8_t)0);   //0x0001 answer is class IN (internet address)
  _udp.write((uint8_t)1);

  _udp.write((unsigned char*)&_ttl, 4);

    // Length of RData is 4 bytes (because, in this case, RData is IPv4)
 _udp.write((uint8_t)0);
  _udp.write((uint8_t)4);
  _udp.write(_resolvedIP, sizeof(_resolvedIP));
  _udp.endPacket();
}

I don't have Windows or Android systems to test, but I can at least confirm that it compiles and doesn't break my IOS connections.

@TimeTravelingOwls
Copy link

@knovoselic - IOS is working fine, with a couple of limitations. I modified your approach slightly, so that handleNotFound tests for a token and either sends the magic "Success" code or just redirects to handleRoot:

void handleNotFound() {
  if (!auth) {
    auth = 1;
// Auto launch browser? <a href="http://www.domain.com/" target="_system">Link Text</a>
//  <a href=\'/handleRoot\' target="_system">Link Text</a>
    webServer.send(200, "text/html", "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>");
  }
  handleRoot();
}

I set the auth token with a preliminary version of the WiFiEvent handler:

// SoftAP status check code
extern "C" {
  #include "user_interface.h"
  #include "ip_addr.h"
  #include "mem.h"

  void _onWiFiEvent(System_Event_t *event){
    if(event->event == EVENT_SOFTAPMODE_STACONNECTED){
      Event_SoftAPMode_StaConnected_t *data = (Event_SoftAPMode_StaConnected_t *)(&event->event_info.sta_connected);
      Serial.printf("Station Connected: id: %d, mac: " MACSTR "\n", data->aid, MAC2STR(data->mac));
    } else if(event->event == EVENT_SOFTAPMODE_STADISCONNECTED){
      Event_SoftAPMode_StaDisconnected_t *data = (Event_SoftAPMode_StaDisconnected_t *)(&event->event_info.sta_disconnected);
      Serial.printf("Station Disconnected: id: %d, mac: " MACSTR "\n", data->aid, MAC2STR(data->mac));
      auth = 0;
      Serial.print("\t\t\t auth = ");
      Serial.println(auth);
    }
  }
}

This seems to work pretty well, but, as you can probably guess, it only works for one simultaneous user. I need to spend a few minutes with the event handler and tag clients by MAC (or something similar), so I can toggle auth by client, and not just connection.

You may want to check out the mods for Android suggested by @Blackie on the community site: http://www.esp8266.com/viewtopic.php?f=32&t=3618&hilit=dnsserver&start=30 He and @SwiCago say that it's now working on Android and Windows.

We're at the start of a long holiday weekend here in the US, but I hope to find some time to work on the IOS part early next week.

@marciodel
Copy link

It is working on Windows and Android with the suggested mod! Thank you very much!

@TimeTravelingOwls
Copy link

@marciodel - Glad to hear it!

If you haven't already, try inserting this line in the <head> of your pages:
<meta name='viewport' content='initial-scale=1'>

It makes a huge difference for IOS devices - it forces them to load your pages at actual size, instead of reduced, so you don't have to pinch to read. I think it works with Android, too, but I haven't been able to test it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants