yoursunny blog
computer, Internet, and life

Login into Captive Portal on ESP8266

Junxiao Shi
2017-07-22

A captive portal is a web page displayed to newly connected users before they are granted broader access to network resource. ESP8266, when configured as a WiFi access point, can serve a captive portal. On the other side of the spectrum, ESP8266 can be used as a WiFi client (aka STAtion), and it should be able to "click through" a captive portal as well.

Clearwave Solutions LLC provides WiFi service at my apartment. Their WiFi network shows a captive portal to each connected client once a week. The captive portal page has a giant Connect button, which instantly enables Internet access.

Clearwave captive portal

Having to press the Connect button every week is an annoyance on a computer or mobile phone. When it comes to little devices such as a temperature and humidity sensor, the existence of captive portal is a bigger problem because the ESP8266 does not have a web browser, so it is impossible for me press the Connect button. However, from network point of view, as long as the ESP8266 sends the correct packets, the WiFi gateway would think the button has been pressed.

To click through the captive portal without a web browser on the ESP8266, I just need to:

  1. Figure out what packets a client sends when pressing the Connect button.
  2. Write a program to send those packets from the ESP8266.

Fiddler: Capture the Packets from Captive Portal

Fiddler is a web debugging proxy. It runs on Windows, and can capture HTTP and HTTPS traffic from any web browser or other web clients. When my laptop is presented the captive portal, I opened Fiddler and captured an interaction.

interaction with captive portal captured by Fiddler

Download: raw Fiddler session archive

From the traffic capture, I can see that when I attempted to access http://captive.apple.com, I'm redirected to a page on 104.236.150.115:8880, and a cookie is set under that domain. Pressing the Connect button triggers a POST to http://104.236.150.115:8880/guest/s/c3o3zy94/login, after which I'm redirected two more times until reaching the original requested web page http://captive.apple.com.

Arduino Sketch to Click Through the Captive Portal

captiveLogin function uses ESP8266HTTPClient library to send HTTP requests to emulate clicking through a captive portal. It performs the same steps as seen in the Fiddler traffic capture.

ESP8266HTTPClient by default discards HTTP response headers in order to save memory. Since we need to obtain redirected URI and cookies, HTTPClient::collectHeaders is called so that HTTPClient class saves Location and Set-Cookie headers when parsing HTTP response headers.

Near the end of captiveLogin function, Internet connectivity is tested with both http://captive.apple.com/ and http://portquiz.net:8080/. The reason of introducing the latter is that, the WiFi gateway sometimes still returns HTTP 302 status when port 80 is accessed immediately after "pressing" the Connect button, but access to other ports is not affected.

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 "ChangeMac.hpp"

const char* ssid     = "UniversityArms-WiFi";
const char* password = "<the-password>";

bool captiveLogin() {
  static const char* LOCATION = "Location";
  static const char* SET_COOKIE = "Set-Cookie";
  static const char* HEADER_NAMES[] = {LOCATION, SET_COOKIE};

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

  String cookie;
  {
    HTTPClient http;
    http.begin(uri);
    http.collectHeaders(HEADER_NAMES, 2);
    int httpCode = http.GET();
    if (httpCode != 200 || !http.hasHeader(SET_COOKIE)) {
      return false;
    }
    cookie = http.header(SET_COOKIE);
    Serial.print("cookie=");
    Serial.println(cookie);
    delay(3000);
  }

  {
    int pos = uri.lastIndexOf("/?");
    if (pos < 0) {
      return false;
    }
    HTTPClient http;
    http.begin(uri.substring(0, pos) + "/login");
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Cookie", cookie);
    http.collectHeaders(HEADER_NAMES, 2);
    int httpCode = http.POST("connect=Connect");
    if (httpCode != 302 || !http.hasHeader(LOCATION)) {
      return false;
    }
    uri = http.header(LOCATION);
    cookie = http.header(SET_COOKIE);
    Serial.print("redirect=");
    Serial.println(uri);
    delay(500);
  }

  {
    HTTPClient http;
    http.begin(uri);
    int httpCode = http.GET();
    if (httpCode != 302) {
      return false;
    }
    delay(500);
  }

  {
    HTTPClient http;
    http.begin("http://captive.apple.com/");
    int httpCode = http.GET();
    if (httpCode == 200) {
      return true;
    }
  }

  {
    HTTPClient http;
    http.begin("http://portquiz.net:8080/");
    int httpCode = http.GET();
    return httpCode == 200;
  }
}

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, password);
  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() {
}

Testing

To test this sketch, and have enough logs in case something goes wrong, it is a good idea to enable debug logs in Arduino IDE:

Arduino IDE debug menu

On the console log, I can see that the ESP8266 has successfully clicked through the captive portal and connected to the Internet.

serial console page1

serial console page2

serial console page3

Tags: ESP8266 Fiddler WiFi

Related

  • Get Online in McDonald's with ESP8266
  • How to Change the MAC Address of ESP8266?
  • "freewifi" via ESP8266 Captive Portal
  • Do Evil with ESP8266: Slow Down the WiFi
  • Rename WiFi Interface on Ubuntu 20.04

Recent

  • NeoPixels on Fipsy FPGA
  • 华盛顿周边一日游 2023-01-07 语言博物馆、西瓜屋
  • 华盛顿周边一日游 2023-06-03 纳雄耐尔港(National Harbor) 🎡
  • 华盛顿周边一日游 2023-04-09 查尔斯县,一日五公园
  • 华盛顿周边一日游 2023-07-01 Kenilworth 水生植物园、华盛顿界碑、加拿大日烟花

Archives

  • 2025 1
  • 2024 2
  • 2023 8
  • 2022 2
  • 2021 30
  • 2020 22
  • 2019 20
  • 2018 28
  • 2017 42
  • 2016 14
  • 2015 1
  • 2014 2
  • 2013 1
  • 2012 1
  • 2011 5
  • 2010 4
  • 2009 10
  • 2008 13
  • 2007 13
  • 2006 8
  • 2005 6
  • 2004 4
  • 2003 5
  • 2002 8
  • 2001 1
  • 1998 1

Tags

  • .Net 4
  • API 4
  • Air602 2
  • Arduino 5
  • Arizona 23
  • C++ 5
  • CHIP 5
  • CSS 3
  • China 15
  • DNS 2
  • Debian 7
  • Docker 2
  • ESP32 7
  • ESP8266 20
  • FPGA 4
  • Fiddler 5
  • Fipsy 3
  • Go 4
  • IPv6 7
  • JavaScript 17
  • LaTeX 2
  • Linux 16
  • Losant 6
  • Maryland 12
  • NDN 34
  • NDNts 10
  • OpenCV 3
  • OpenWrt 2
  • Oregon 13
  • PHP 10
  • Python 4
  • RaspberryPi 8
  • TV 2
  • Ubuntu 15
  • VPN 2
  • VirtualBox 3
  • WashingtonDC 2
  • WiFi 8
  • Windows 7
  • Wireshark 4
  • academic 7
  • bash 4
  • commentary 21
  • fiction 4
  • food 5
  • geocaching 22
  • git 4
  • hosting 15
  • life 41
  • nRF52 2
  • ndn6 2
  • physics 2
  • security 8
  • travel 29
  • web 15
©2025 yoursunny.com