Skip to content

Commit b404b35

Browse files
committed
examples: add captive portal example. Resolve #97
1 parent daf6ca1 commit b404b35

File tree

3 files changed

+186
-1
lines changed

3 files changed

+186
-1
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
New functionality:
66

7-
7+
* #97: New example: Captive Portal
88

99
Bug fixes:
1010

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ You will find several examples showing how you can use the library (roughly orde
8585
- [Self-Signed-Certificate](examples/Self-Signed-Certificate/Self-Signed-Certificate.ino): Shows how to generate a self-signed certificate on the fly on the ESP when the sketch starts. You do not need to run `create_cert.sh` to use this example.
8686
- [Middleware](examples/Middleware/Middleware.ino): Shows how to use the middleware API for logging. Middleware functions are defined very similar to webservers like Express.
8787
- [Authentication](examples/Authentication/Authentication.ino): Implements a chain of two middleware functions to handle authentication and authorization using HTTP Basic Auth.
88+
- [Captive-Portal](examples/Captive-Portal/Captive-Portal.ino): Very basic captive portal implementation. An AP is created that uses DNS redirects to lead all connected clients to a specific website. You do not need to run `create_cert.sh` to use this example.
8889
- [Websocket-Chat](examples/Websocket-Chat/Websocket-Chat.ino): Provides a browser-based chat built on top of websockets. **Note:** Websockets are still under development!
8990
- [REST-API](examples/REST-API/REST-API.ino): Uses [ArduinoJSON](https://arduinojson.org/) and [SPIFFS file upload](https://github.com/me-no-dev/arduino-esp32fs-plugin) to serve a small web interface that provides a REST API.
9091

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Example for the ESP32 HTTP(S) Webserver
3+
*
4+
* This script will create a captive portal. A captive portal is an access point
5+
* that resolves all DNS requests to a specific IP address (in this case its own)
6+
* where it hosts a webserver. Then, it redirects the user to a well-known hostname
7+
* from where it serves a website.
8+
*
9+
* Usually this is used for providing a login page in a public WiFi network. For
10+
* that specific use case, it exists an API:
11+
* https://tools.ietf.org/html/draft-ietf-capport-api-08
12+
* However, this approach needs an upstream internet connection and valid certificated.
13+
*
14+
* Another option is to use DHCP option 114 to provide the URL of a captive portal, but
15+
* configuring custom option types for the DHCP server is a bit tricky in Arduino.
16+
*
17+
* So this is really the basic example: We will redirect users to a website when they're
18+
* connected to the access point. If the client has other means of connecting to
19+
* the Internet available, this might not work, as external DNS servers might be in use.
20+
* This server will not redirect the client to the portal.
21+
*
22+
* Please also note that the Arduino DNS server is quite hacky. It can only process very
23+
* specific requests, so it might not work for every client (in particular: if you like
24+
* to use "dig" for debugging, don't.)
25+
*/
26+
27+
// C O N F I G U R A T I O N - - - - - - - - - - - - - - - - - -
28+
// The hostname to redirect to.
29+
// You can use either a hostname (arbitrary, like "captive.esp") or an IP address. Using
30+
// the IP address is preferable, as this circumvents the issue that the client resolves
31+
// the local hostname on an external DNS and will not be guided to the captive portal.
32+
// Must start with http://
33+
const char *hosturl = "http://192.168.8.1";
34+
35+
// The name of the access point
36+
const char *apname = "CaptiveESP";
37+
38+
// Subnet configuration. When using an IP as hostname, make sure it is the same as awip.
39+
IPAddress apip(192, 168, 8, 1);
40+
IPAddress gwip(192, 168, 8, 1);
41+
IPAddress apnetmask(255, 255, 255, 0);
42+
43+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
44+
45+
// We will use wifi
46+
#include <WiFi.h>
47+
48+
// We need to run a DNS server
49+
#include <DNSServer.h>
50+
51+
// Required for the middleware
52+
#include <functional>
53+
54+
// We include the server. For the captive portal, we will use the HTTP server.
55+
#include <HTTPServer.hpp>
56+
#include <HTTPRequest.hpp>
57+
#include <HTTPResponse.hpp>
58+
59+
// The server comes in a separate namespace. For easier use, include it here.
60+
using namespace httpsserver;
61+
62+
// We instantiate the web server with the default parameters
63+
HTTPServer portalServer = HTTPServer();
64+
65+
// Same for the DNS server
66+
DNSServer dnsServer = DNSServer();
67+
68+
// We only define a single page to show the general operation of the captive portal.
69+
// For 404 etc, we use the webserver default. You can have a look at the others
70+
// examples to make your project fancier.
71+
void handleRoot(HTTPRequest * req, HTTPResponse * res);
72+
73+
// This middleware intercepts every request. If it notices that the request is not
74+
// for the target host (configured above), it will send a redirect.
75+
void captiveMiddleware(HTTPRequest * req, HTTPResponse * res, std::function<void()> next);
76+
77+
void setup() {
78+
// For logging
79+
Serial.begin(115200);
80+
81+
// 1) Configure the access point
82+
// Depending on what you did before with your ESP, you might face the problem of
83+
// getting GURU medidations every time some client connects to the access point.
84+
// At the end of this sketch, you'll find instructions for a workaround.
85+
Serial.print("Setting up WiFi... ");
86+
// Disable STA mode, if still active
87+
WiFi.disconnect();
88+
// Do not uses connection config persistence
89+
WiFi.persistent(false);
90+
// Start the SoftAP
91+
WiFi.softAP(apname);
92+
// Reconfigure the AP IP. Wait until it's done.
93+
while (!(WiFi.softAPIP() == apip)) {
94+
WiFi.softAPConfig(apip, gwip, apnetmask);
95+
}
96+
Serial.println("OK");
97+
98+
// 2) Configure DNS
99+
// All hostnames are belong to us (we let a wildcard point to our AP)
100+
Serial.print("Starting DNS... ");
101+
if (!dnsServer.start(53, "*", apip)) {
102+
Serial.println("failed");
103+
while(true);
104+
}
105+
Serial.println("OK");
106+
107+
// 3) Configure the server
108+
// We create the single node and store it on the server.
109+
portalServer.registerNode(new ResourceNode("/", "GET", &handleRoot));
110+
// We also register our middleware
111+
portalServer.addMiddleware(&captiveMiddleware);
112+
// Then we start the server
113+
Serial.print("Starting HTTP server... ");
114+
portalServer.start();
115+
if (portalServer.isRunning()) {
116+
Serial.println("OK");
117+
} else {
118+
Serial.println("failed");
119+
while(true);
120+
}
121+
}
122+
123+
void loop() {
124+
// In the main loop, we now need to process both, DNS and HTTP
125+
portalServer.loop();
126+
dnsServer.processNextRequest();
127+
}
128+
129+
// This function intercepts each request. See the middleware-examples for more details.
130+
// The goal is to identify whether the client has already been redirected to the configured
131+
// hostname, and if not, to trigger this redirect. Being redirected on arbitrary domains
132+
// is a way how some operating systems detect the presence of a captive portal.
133+
void captiveMiddleware(HTTPRequest * req, HTTPResponse * res, std::function<void()> next) {
134+
// To check if we have already redirected, we need the "host" HTTP header
135+
HTTPHeaders *headers = req->getHTTPHeaders();
136+
std::string hdrHostname = headers->getValue("host");
137+
// If the hostname is not what we redirect to...
138+
if (hdrHostname != &hosturl[7]) { // cutoff the http://
139+
// ... we start a temporary redirect ...
140+
res->setStatusCode(302);
141+
res->setStatusText("Found");
142+
// ... to this hostname ...
143+
res->setHeader("Location", hosturl);
144+
// ... and stop processing the request.
145+
return;
146+
}
147+
// Otherwise, the request will be forwarded (and most likely reach the handleRoot function)
148+
next();
149+
}
150+
151+
// Main page of the captive portal
152+
void handleRoot(HTTPRequest * req, HTTPResponse * res) {
153+
// Status code is 200 OK by default.
154+
// We want to deliver a simple HTML page, so we send a corresponding content type:
155+
res->setHeader("Content-Type", "text/html");
156+
res->println(
157+
"<!DOCTYPE html>"
158+
"<html>"
159+
"<head><title>Captive Portal</title></head>"
160+
"<body>"
161+
"<h1>Captive Portal</h1>"
162+
"<p>Ha, gotcha!</p>"
163+
"</body>"
164+
"</html>"
165+
);
166+
}
167+
168+
// Workaround for GURU meditation on new connections to the ESP32
169+
// The most likely reason is some broken WiFi configuration in the flash of your ESP32
170+
// that does not go away, even with WiFi.persistent(false) or re-flashing the sketch.
171+
// This broken configuration resides in the nvm partition of the ESP.
172+
//
173+
// Partitions on the ESP are a bit different from what you know from your computer. They
174+
// have a specific type and can contain either data, configuration or an application image.
175+
// The WiFi configuration is placed in the "nvm" partition.
176+
//
177+
// If you know how to use esptool, read the partition table from 0x8000..0x9000, look for
178+
// the nvm partition and clear it using esptool erase_region <offset> <length>
179+
//
180+
// If you are not familiar with the esptool, you can erase the whole flash.
181+
// Make sure only one board is connected to your computer, then run:
182+
// esptool.py erase_flash
183+
// After that, flash your sketch again.
184+
// You get esptool from https://github.com/espressif/esptool

0 commit comments

Comments
 (0)