ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
captive_portal.cpp
Go to the documentation of this file.
1#include "captive_portal.h"
2#ifdef USE_CAPTIVE_PORTAL
3#include "esphome/core/log.h"
9#ifdef USE_PROVISIONING
11#endif
12#include "captive_index.h"
13
15
16static const char *const TAG = "captive_portal";
17
18void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
19 AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
20 stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
21 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
22 const char *mac_str = get_mac_address_pretty_into_buffer(mac_s);
23#ifdef USE_ESP8266
24 stream->print(ESPHOME_F("{\"mac\":\""));
25 stream->print(mac_str);
26 stream->print(ESPHOME_F("\",\"name\":\""));
27 stream->print(App.get_name().c_str());
28 stream->print(ESPHOME_F("\",\"aps\":[{}"));
29#else
30 stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str());
31#endif
32
33 // An SSID can contain a " or \ that would break the JSON, so escape it before writing it out. An SSID is at most
34 // 32 bytes (IEEE 802.11), so this is large enough that nothing is ever dropped. Reused for every scan result.
35 char escaped_ssid[32 * JSON_ESCAPE_MAX_EXPANSION + 1];
36 {
37 // Invariant: only bounded in-memory work under the lock; the network send
38 // happens later in request->send()
40 const auto &results = wifi::global_wifi_component->get_scan_result();
41 for (const auto &scan : results) {
42 bool with_auth = false;
43 if (!wifi::should_show_scan_entry(results, scan, with_auth))
44 continue;
45
46 json_escape_into_buffer(escaped_ssid, scan.get_ssid());
47#ifdef USE_ESP8266
48 stream->print(ESPHOME_F(",{\"ssid\":\""));
49 stream->print(escaped_ssid);
50 stream->print(ESPHOME_F("\",\"rssi\":"));
51 stream->print(scan.get_rssi());
52 stream->print(ESPHOME_F(",\"lock\":"));
53 stream->print(with_auth);
54 stream->print(ESPHOME_F("}"));
55#else
56 stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), with_auth);
57#endif
58 }
59 }
60 stream->print(ESPHOME_F("]}"));
61 request->send(stream);
62}
63void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
64 const auto &ssid = request->arg("ssid");
65 const auto &psk = request->arg("psk");
66 ESP_LOGI(TAG,
67 "Requested WiFi Settings Change:\n"
68 " SSID='%s'\n"
69 " Password=" LOG_SECRET("'%s'"),
70 ssid.c_str(), psk.c_str());
71#ifdef USE_ESP8266
72 // ESP8266 is single-threaded, call directly
73 wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
74#else
75 // Defer save to main loop thread to avoid NVS operations from HTTP thread
76 this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
77#endif
78 request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting..."));
79}
80
82 // Disable loop by default - will be enabled when captive portal starts
83 this->disable_loop();
84#ifdef USE_PROVISIONING
85 // The captive portal is a provisioning surface: once the provisioning window
86 // has closed, stop serving it. WiFi's own closed-callback shuts down the
87 // access point the portal runs on, and the gated fallback in WiFiComponent's
88 // loop() ensures neither is started again afterwards.
91 if (this->active_) {
92 ESP_LOGD(TAG, "Provisioning window closed; stopping captive portal");
93 this->end();
94 }
95 });
96 }
97#endif
98}
100 this->base_->init();
101 if (!this->initialized_) {
102 this->base_->add_handler_without_auth(this);
103 }
104
106
107#if defined(USE_ESP32)
108 // Create DNS server instance for ESP-IDF
109 this->dns_server_ = make_unique<DNSServer>();
110 this->dns_server_->start(ip);
111#elif defined(USE_ARDUINO)
112 this->dns_server_ = make_unique<DNSServer>();
113 this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
114 this->dns_server_->start(53, ESPHOME_F("*"), ip);
115#endif
116
117 this->initialized_ = true;
118 this->active_ = true;
119
120 // Enable loop() now that captive portal is active
121 this->enable_loop();
122
123 ESP_LOGV(TAG, "Captive portal started");
124}
125
126void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
127#ifdef USE_ESP32
128 char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
129 StringRef url = req->url_to(url_buf);
130#else
131 const auto &url = req->url();
132#endif
133 if (url == ESPHOME_F("/config.json")) {
134 this->handle_config(req);
135 return;
136 } else if (url == ESPHOME_F("/wifisave")) {
137 this->handle_wifisave(req);
138 return;
139 }
140
141 // All other requests get the captive portal page
142 // This includes OS captive portal detection endpoints which will trigger
143 // the captive portal when they don't receive their expected responses
144#ifndef USE_ESP8266
145 auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
146#else
147 auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
148#endif
149#ifdef USE_CAPTIVE_PORTAL_GZIP
150 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
151#else
152 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("br"));
153#endif
154 req->send(response);
155}
156
159 // Before WiFi
160 return setup_priority::WIFI + 1.0f;
161}
162void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); }
163
164CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
165
166} // namespace esphome::captive_portal
167
168#endif
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
void enable_loop()
Enable this component's loop.
Definition component.h:246
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void disable_loop()
Disable this component's loop.
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
std::unique_ptr< DNSServer > dns_server_
CaptivePortal(web_server_base::WebServerBase *base)
void handle_config(AsyncWebServerRequest *request)
web_server_base::WebServerBase * base_
void handleRequest(AsyncWebServerRequest *req) override
void handle_wifisave(AsyncWebServerRequest *request)
void add_handler_without_auth(AsyncWebHandler *handler)
WARNING: Registers a handler that bypasses the USE_WEBSERVER_AUTH middleware.
Guards WiFiComponent::scan_result_.
void save_wifi_sta(const std::string &ssid, const std::string &password)
const wifi_scan_vector_t< WiFiScanResult > & get_scan_result() const
Main-loop callers may read this directly.
CaptivePortal * global_captive_portal
ProvisioningManager * global_provisioning_manager
constexpr float WIFI
Definition component.h:50
bool should_show_scan_entry(const Results &results, const Entry &scan, bool &with_auth)
Definition scan_list.h:11
WiFiComponent * global_wifi_component
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:832
const char * json_escape_into_buffer(std::span< char > buf, StringRef value, bool short_control_escapes)
Copy value into buf, escaping the characters that cannot appear raw inside a JSON string literal.
Definition helpers.cpp:357
Application App
Global storage of Application pointer - only one Application can exist.
SemaphoreHandle_t lock