DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
provisioning_service.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file provisioning_service.cpp
6 * @brief First-boot WiFi provisioning / captive portal (DETWS_ENABLE_PROVISIONING).
7 *
8 * The catch-all DNS responder uses the transport-layer UDP service (no add-on library);
9 * credentials persist to NVS via Preferences.
10 */
11
15#include <string.h>
16
17// ---------------------------------------------------------------------------
18// Form-field parser (always compiled; the only non-trivial logic, unit-tested).
19// ---------------------------------------------------------------------------
20
21bool detws_prov_form_field(const char *body, const char *key, char *out, size_t cap)
22{
23 if (out && cap)
24 out[0] = '\0';
25 if (!body || !key || !out || cap == 0)
26 return false;
27
28 size_t klen = strnlen(key, cap); // a form field name longer than the value buffer cannot yield a value
29 const char *val = nullptr;
30 for (const char *p = body; *p; p++)
31 {
32 // Match the key only as a whole field name: at the start or after '&'.
33 if ((p == body || p[-1] == '&') && strncmp(p, key, klen) == 0 && p[klen] == '=')
34 {
35 val = p + klen + 1;
36 break;
37 }
38 }
39 if (!val)
40 return false;
41
42 size_t o = 0;
43 for (const char *q = val; *q && *q != '&'; q++)
44 {
45 char c = *q;
46 if (c == '+')
47 {
48 c = ' ';
49 }
50 else if (c == '%')
51 {
52 int h = det_hex_val(q[1]);
53 int l = (h >= 0) ? det_hex_val(q[2]) : -1;
54 if (h >= 0 && l >= 0)
55 {
56 c = (char)((h << 4) | l);
57 q += 2;
58 }
59 }
60 if (o + 1 < cap)
61 out[o++] = c;
62 else
63 break;
64 }
65 out[o] = '\0';
66 return true;
67}
68
69// ---------------------------------------------------------------------------
70// ESP32 captive portal (softAP + lwIP UDP DNS + form/save routes)
71// ---------------------------------------------------------------------------
72
73#if DETWS_ENABLE_PROVISIONING && defined(ARDUINO)
74
75#include "dwserver.h"
78#include <Arduino.h>
79#include <Preferences.h>
80#include <WiFi.h>
81
82// All provisioning-service state, owned by one instance (internal linkage): the server handle
83// and the softAP IP the captive-portal DNS answers with. Grouped so it is one named owner,
84// unreachable cross-TU.
85struct ProvCtx
86{
87 DetWebServer *server = nullptr;
88 uint8_t ap_ip[4] = {192, 168, 4, 1};
89};
90static ProvCtx s_prov;
91
92// The NVS namespace + credential keys (DETWS_PROV_NVS_NAMESPACE / _KEY_SSID / _KEY_PSK) live in
93// ServerConfig.h under DETWS_ENABLE_PROVISIONING so a deployment can override them; used across
94// the read / clear / save paths (and, for ssid/psk, as the HTML form field names).
95
96// Catch-all DNS: answer every query with our softAP IP (captive-portal hijack).
97static void prov_dns_recv(const uint8_t *req, size_t qlen, struct DetUdpPeer *peer, void *ctx)
98{
99 (void)ctx;
100 if (qlen < 12)
101 return; // smaller than a DNS header
102
103 // Walk the (single) question to find where it ends: labels until a 0 byte,
104 // then 2-byte QTYPE + 2-byte QCLASS.
105 size_t qend = 12;
106 while (qend < qlen && req[qend] != 0)
107 qend += (size_t)req[qend] + 1;
108 qend += 1 + 4;
109 if (qend > qlen)
110 return;
111
112 uint8_t resp[300];
113 if (qend + 16 > sizeof(resp))
114 return;
115 memcpy(resp, req, qend);
116 resp[2] = 0x81; // QR=1, opcode 0, RD copied
117 resp[3] = 0x80; // RA=1, RCODE 0
118 resp[6] = 0x00;
119 resp[7] = 0x01; // ANCOUNT = 1
120 resp[8] = resp[9] = resp[10] = resp[11] = 0x00;
121
122 size_t n = qend;
123 resp[n++] = 0xC0; // name: pointer to the question at offset 0x0C
124 resp[n++] = 0x0C;
125 resp[n++] = 0x00;
126 resp[n++] = 0x01; // TYPE A
127 resp[n++] = 0x00;
128 resp[n++] = 0x01; // CLASS IN
129 resp[n++] = 0x00;
130 resp[n++] = 0x00;
131 resp[n++] = 0x00;
132 resp[n++] = 0x3C; // TTL 60s
133 resp[n++] = 0x00;
134 resp[n++] = 0x04; // RDLENGTH 4
135 resp[n++] = s_prov.ap_ip[0];
136 resp[n++] = s_prov.ap_ip[1];
137 resp[n++] = s_prov.ap_ip[2];
138 resp[n++] = s_prov.ap_ip[3];
139
140 det_udp_send(peer, resp, n);
141}
142
143bool detws_provisioning_load(char *ssid, size_t ssid_cap, char *psk, size_t psk_cap)
144{
145 if (ssid && ssid_cap)
146 ssid[0] = '\0';
147 if (psk && psk_cap)
148 psk[0] = '\0';
149 Preferences prefs;
150 if (!prefs.begin(DETWS_PROV_NVS_NAMESPACE, true))
151 return false;
152 String s = prefs.getString(DETWS_PROV_KEY_SSID, "");
153 String k = prefs.getString(DETWS_PROV_KEY_PSK, "");
154 prefs.end();
155 if (s.length() == 0)
156 return false;
157 strncpy(ssid, s.c_str(), ssid_cap - 1);
158 ssid[ssid_cap - 1] = '\0';
159 strncpy(psk, k.c_str(), psk_cap - 1);
160 psk[psk_cap - 1] = '\0';
161 return true;
162}
163
165{
166 Preferences prefs;
167 if (prefs.begin(DETWS_PROV_NVS_NAMESPACE, false))
168 {
169 prefs.clear();
170 prefs.end();
171 }
172}
173
174static void prov_form_handler(uint8_t slot_id, HttpReq *req)
175{
176 (void)req;
177 s_prov.server->send(slot_id, 200, DET_MIME_TEXT_HTML, DETWS_PROV_FORM);
178}
179
180static void prov_save_handler(uint8_t slot_id, HttpReq *req)
181{
182 char ssid[33];
183 char psk[64];
184 bool have_ssid =
185 detws_prov_form_field((const char *)req->body, DETWS_PROV_KEY_SSID, ssid, sizeof(ssid)) && ssid[0] != '\0';
186 detws_prov_form_field((const char *)req->body, DETWS_PROV_KEY_PSK, psk, sizeof(psk));
187 if (!have_ssid)
188 {
189 s_prov.server->send(slot_id, 400, DET_MIME_TEXT_PLAIN, "SSID required");
190 return;
191 }
192 Preferences prefs;
193 prefs.begin(DETWS_PROV_NVS_NAMESPACE, false);
194 prefs.putString(DETWS_PROV_KEY_SSID, ssid);
195 prefs.putString(DETWS_PROV_KEY_PSK, psk);
196 prefs.end();
197 s_prov.server->send(slot_id, 200, DET_MIME_TEXT_HTML, DETWS_PROV_SAVED_HTML);
198 delay(500);
199 ESP.restart();
200}
201
202void detws_provisioning_begin(DetWebServer &server, const char *ap_ssid)
203{
204 s_prov.server = &server;
205 WiFi.mode(WIFI_AP);
206 WiFi.softAP(ap_ssid);
207 IPAddress ip = WiFi.softAPIP();
208 s_prov.ap_ip[0] = ip[0];
209 s_prov.ap_ip[1] = ip[1];
210 s_prov.ap_ip[2] = ip[2];
211 s_prov.ap_ip[3] = ip[3];
212
213 // Catch-all DNS on UDP/53 via the transport-layer UDP service (callback-driven).
214 det_udp_listen(53, prov_dns_recv, nullptr);
215
216 server.on("/save", HttpMethod::HTTP_POST, prov_save_handler);
217 server.on("/*", HttpMethod::HTTP_GET, prov_form_handler); // any other path -> the form
218}
219
220#else // disabled / non-Arduino: stubs (form-field parser above stays available)
221
222bool detws_provisioning_load(char *ssid, size_t ssid_cap, char *psk, size_t psk_cap)
223{
224 if (ssid && ssid_cap)
225 ssid[0] = '\0';
226 if (psk && psk_cap)
227 psk[0] = '\0';
228 return false;
229}
230void detws_provisioning_begin(DetWebServer &server, const char *ap_ssid)
231{
232 (void)server;
233 (void)ap_ssid;
234}
238
239#endif // DETWS_ENABLE_PROVISIONING && ARDUINO
Single-port HTTP server with deterministic, zero-allocation execution.
Definition dwserver.h:348
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
Definition dwserver.cpp:759
Layer 7 (Application) - public HTTP routing API.
@ HTTP_POST
Non-idempotent create / action.
@ HTTP_GET
Safe, idempotent read.
Tiny no-stdlib hex encode / decode / nibble helpers (one shared copy).
int det_hex_val(char c)
Hex digit -> 0..15, or -1 if c is not a hex digit (either case).
Definition hex.h:31
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
void detws_provisioning_begin(DetWebServer &server, const char *ap_ssid)
Start the captive portal: softAP ap_ssid + catch-all DNS + form routes.
bool detws_prov_form_field(const char *body, const char *key, char *out, size_t cap)
Extract and URL-decode a field from an x-www-form-urlencoded body.
bool detws_provisioning_load(char *ssid, size_t ssid_cap, char *psk, size_t psk_cap)
Load stored WiFi credentials from NVS.
void detws_provisioning_clear()
Erase stored credentials (forces re-provisioning on next boot).
First-boot WiFi provisioning via a captive portal (DETWS_ENABLE_PROVISIONING).
Fully-parsed HTTP/1.1 request.
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:178
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
Layer 4 (Transport) - connectionless UDP datagram service.
const char DETWS_PROV_SAVED_HTML[]
Success page shown after credentials are saved and the device reboots.
const char DETWS_PROV_FORM[]
Captive-portal form for WiFi credential entry (provisioning service).
Layer 7 (Application) - embedded web assets generated from src/web/input/.