Get Online in McDonald's with ESP8266

McDonald's used to be the largest chain of free restrooms in the world. Nowadays, they are the largest chain of free WiFi across the United States. Every McDonald's I've been to offers AT&T WiFi services. I can walk in, connect to "attwifi" on my phone, accept the license agreement, and I'm online.

attwifi captive portal in McDonald's

The license agreement page is called a captive portal, a web page displayed to newly connected users before they are granted broader access to network resource. While it's trivial to click through the captive portal on a smartphone, if you are wearing an ESP8266 as a connected jewelry, it would not connect successfully. When I encountered a captive portal in my apartment WiFi, I made an Arduino sketch to send the packets I found through Fiddler, but McDonald's WiFi is different:

  • Each McDonald's store hosts their captive portal on a different domain.
  • The form submission step needs several parameters from the HTML, but does not use cookies.

attwifi captive portal form HTML

attwifi captive portal POST request

With the aid of refillable Diet Coke, I wrote a new Arduino sketch to click through McDonald's WiFi captive portal. It sends four HTTP requests:

  1. GET http://captive.apple.com/, and look at "Location" header to see where AT&T redirect us to.

  2. Follow the redirection. Parse the HTML to find:

    • <form> tag action attribute, which tells us where to submit the form. This is normally a relative URI, and needs to be combined with the hostname of the current request.
    • <input> tag name and value attributes, which contain the form fields to be submitted. This involves escaping the values into percent-encoding format.
  3. Submit the form with a POST request.

  4. Try GET http://captive.apple.com/ again to verify that we are connected.

This code also includes randomizing the MAC address and DHCP hostname, so that I can test the procedure with a "new" client each time I reset the ESP8266. This requires copying a little ChangeMac library into the sketch.

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <StreamString.h>
#include "ChangeMac.hpp"

const char* ssid     = "attwifi";

String extractHtmlAttribute(const String& tag, String attr) {
  attr += "=\"";
  int first = tag.indexOf(attr);
  if (first < 0) {
    return "";
  }
  first += attr.length();

  int last = tag.indexOf("\"", first);
  if (last < 0) {
    return "";
  }

  return tag.substring(first, last);
}

String escapeFormField(const String& input) {
  StreamString escaped;
  for (char ch : input) {
    if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
      escaped.write(ch);
    } else {
      escaped.printf("%%%02x", ch);
    }
  }
  return escaped;
}

bool captiveLogin() {
  static const char* LOCATION = "Location";

  String uri;
  {
    HTTPClient http;
    http.begin("http://captive.apple.com/");
    http.collectHeaders(&LOCATION, 1);
    int httpCode = http.GET();
    if (httpCode == 200) {
      return true;
    }
    if (httpCode != 302 || !http.hasHeader(LOCATION)) {
      Serial.print("Captive error: ");
      Serial.println(httpCode);
      return false;
    }
    uri = http.header(LOCATION);
    http.end();
    Serial.print("portal=");
    Serial.println(uri);
    delay(2000);
  }

  String host;
  {
    int pos = uri.indexOf("://");
    if (pos < 0) {
      return false;
    }
    pos = uri.indexOf("/", pos + 3);
    if (pos < 0) {
      return false;
    }
    host = uri.substring(0, pos);
  }

  String form;
  {
    HTTPClient http;
    http.begin(uri);
    int httpCode = http.GET();
    if (httpCode != 200) {
      Serial.print("Portal error: ");
      Serial.println(httpCode);
      return false;
    }

    delay(100); // allow time to receive HTTP response
    Stream& payload = http.getStream();
    while (payload.available()) {
      String line = payload.readStringUntil('\n');
      if (line.indexOf("<form ") >= 0) {
        Serial.print("form-tag=");
        Serial.println(line);
        uri = host + extractHtmlAttribute(line, "action");
        Serial.print("submit=");
        Serial.println(uri);
        continue;
      }
      if (line.indexOf("<input ") < 0) {
        continue;
      }
      Serial.print("input-tag=");
      Serial.println(line);
      String name  = extractHtmlAttribute(line, "name");
      String value = extractHtmlAttribute(line, "value");
      String escaped = escapeFormField(value);
      Serial.print("name=");
      Serial.print(name);
      Serial.print(" value=");
      Serial.print(value);
      Serial.print(" escaped=");
      Serial.println(escaped);

      if (form.length() > 0) {
        form += "&";
      }
      form += name + "=" + escaped;
    }
    http.end();
    delay(3000);
  }

  {
    Serial.println("form:");
    Serial.println(form);
    HTTPClient http;
    http.begin(uri);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    int httpCode = http.POST(form);
    if (httpCode != 200) {
      Serial.print("Submit error: ");
      Serial.println(httpCode);
      return false;
    }
    http.end();
    delay(500);
  }

  {
    HTTPClient http;
    http.begin("http://captive.apple.com/");
    int httpCode = http.GET();
    if (httpCode != 200) {
      Serial.print("Verify error: ");
      Serial.println(httpCode);
      return false;
    }
    http.end();
    return true;
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.persistent(false);

  uint8_t mac[6];
  makeRandomMac(mac);
  changeMac(mac);
  Serial.print("MAC address is ");
  Serial.println(WiFi.macAddress());

  String hostname = "iPad-";
  hostname += random(10);
  hostname += random(10);
  hostname += random(10);
  hostname += random(10);
  WiFi.hostname(hostname);
  Serial.print("Hostname is ");
  Serial.println(hostname);

  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(WiFi.status());
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  if (!captiveLogin()) {
    ESP.restart();
  }
}

void loop() {
  HTTPClient http;
  http.begin("http://www.rfc-editor.org/rfc/rfc868.txt");
  int httpCode = http.GET();
  if (httpCode == 200) {
    Serial.println(http.getString());
  } else {
    Serial.print("HTTP error ");
    Serial.println(httpCode);
  }
  http.end();
  delay(60000);
}

Execution log indicates the ESP8266 has successfully connected to the Internet, and is able to download a text file from the web.

serial console log