cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
provision.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
11
12/******************************************************************
13 * 1. Included files
14 ******************************************************************/
15
16#include "provision.h"
17
18#include <atomic>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22
23#include "freertos/FreeRTOS.h"
24#include "freertos/task.h"
25#include "freertos/semphr.h"
26
27/* Before lwip/sockets.h, and it has to stay there. CW_Utils.h drags in
28 * Arduino's IPAddress.h, which declares `extern const IPAddress INADDR_NONE;`.
29 * Once lwIP's headers have been seen, INADDR_NONE is a macro expanding to a
30 * u32_t cast, and that declaration stops parsing. Arduino first, lwIP second. */
31#include "CW_Utils.h"
32
33#include "lwip/sockets.h"
34
35#include "esp_heap_caps.h" /* largest free block — fragmentation, not just total */
36#include "esp_http_server.h"
37#include "esp_log.h"
38#include "esp_mac.h"
39#include "esp_random.h"
40#include "esp_system.h" /* esp_restart — the network switch reboots to apply */
41#include "esp_timer.h"
42#include "nvs.h"
43#include "mbedtls/sha256.h" /* Tron base58check */
44
45#include "addr_check.h"
46#include "eth_addr.h"
47#include "form_parse.h"
48#include "json_out.h"
49#include "net.h"
50#include "ota.h"
51#include "portal_page.h" /* PAGE_HTML + PAGE_JS — the document page_get serves */
52#include "settings.h"
53#include "settings_rules.h" /* fee bounds, time-zone range — host-tested */
54#include "civil_time.h" /* CIVIL_DST__COUNT — the clock's DST rules */
55
56static const char *const TAG = "prov";
57
58/******************************************************************
59 * 2. Constants
60 ******************************************************************/
61
62/* PORTAL_IP / PORTAL_URL are in portal_page.h: the page prints the URL for an
63 * operator to type and the handlers below redirect to it, so the two have to
64 * agree and there is one definition of it. */
65
66#define AP_PASS_LEN 10U /* ~50 bits out of the 32-char alphabet below */
67
68/* No 0/O/1/I/l: this is read off a 2.8" panel and typed by hand when the QR
69 * code will not scan, and those four are where that goes wrong. */
70static const char AP_PASS_ALPHABET[] = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
71
72/* Own namespace, so nothing this module keeps is swept up by anything that
73 * erases settings for other reasons. settings_factory_reset() erases this one BY
74 * NAME — grep "prov" in settings.cpp before renaming it. */
75#define NS_PROV "prov"
76
77/* The AP passphrase, drawn once and kept — see ap_pass_load(). */
78#define K_AP_PASS "ap_pass"
79
80/* Keys nothing writes any more; erased on sight. See ap_pass_load(). */
81#define K_TLS_CRT "tls_crt"
82#define K_TLS_KEY "tls_key"
83
84/* Gas-cap bounds, in Gwei: FEE_GWEI_MIN / _MAX in settings_rules.h. The numbers
85 * the panel's +/- steppers used to enforce before the fees moved to this page —
86 * kept identical so a value stored here is one the terminal has always been able
87 * to hold. The page's own min/max attributes say the same thing to the browser;
88 * the two constants are what actually decide. */
89
90/* Read this much of an upload at a time. 4 KB is a flash page-erase unit and one
91 * lwIP window's worth, and it lives in .bss rather than on the httpd task's
92 * stack — where it would not fit. ota.h refuses a second concurrent upload, so
93 * one shared buffer is enough. */
94#define UPLOAD_CHUNK 4096U
95
96/* Consecutive recv timeouts before an upload is declared dead. cfg.recv_wait_timeout
97 * is 30 s, so this is two minutes of a socket saying nothing at all — far longer
98 * than any pause a slow phone puts between chunks, and it is what stops a stalled
99 * transfer from holding the admin page open past its window (see the deadline
100 * check in ui.cpp, which does not close the page while bytes are arriving). */
101#define UPLOAD_MAX_STALLS 4U
102
103#define TOKEN_HEX_LEN 32U /* 128 bits of session token */
104
105#define PROV_MAX_APS 16U
106
107/* No release-list URL here any more, and no "check for updates" button on the
108 * page. The portal is served on the terminal's own SoftAP with the station
109 * interface down (see prov_start), so the phone reading this page has no route
110 * to a release list — a check button could only ever report a network error.
111 * The file picker is the whole update story: fetch the signed image on a device
112 * that does have internet, then hand it to the terminal here. */
113
114/******************************************************************
115 * 3. Module state
116 ******************************************************************/
117
118static httpd_handle_t s_httpd = NULL;
119static TaskHandle_t s_dns_task = NULL;
120static volatile bool s_dns_run = false;
121static ui_event_cb_t s_cb = NULL;
122
123/* Atomic because prov_stop() has two callers on two tasks — the UI task (the
124 * portal card's Done button, a declined firmware image, the window deadline) and
125 * the main task (the wizard finishing, a value committed from the admin page). Two
126 * of those landing in the same tick would otherwise both get past the "already
127 * off?" test and call httpd_stop() twice on one handle. prov_stop() claims the mode
128 * with an exchange, so exactly one caller does the teardown. */
129static std::atomic<prov_mode_t> s_mode{PROV_MODE_OFF};
131static int64_t s_deadline_us = 0; /* 0 = no self-close */
132
133static char s_ssid[33] = "";
134static char s_pass[AP_PASS_LEN + 1U] = "";
135static char s_qr[96] = "";
136
137static uint8_t s_upload[UPLOAD_CHUNK];
138
139/* Browser session. The token is minted when a browser asks to be authorised and
140 * only becomes usable once somebody types the admin code on the panel, so a
141 * second browser on the same network gets a token that authorises nothing. */
142static char s_token[TOKEN_HEX_LEN + 1U] = "";
143static volatile bool s_auth_pending = false;
144static volatile bool s_authed = false;
145
146/* Wi-Fi-only re-join: no admin code, no numbered steps. See prov_set_wifi_only(). */
147static volatile bool s_wifi_only = false;
148
149/* A line for the page from the one party that knows why something did not work.
150 * Written by the main task, read by the HTTP task. */
151static char s_note[128] = "";
152
153/* Guards s_note and the scan list below: both are written by the main task and
154 * read by the HTTP task, and a torn scan record is an SSID glued to the wrong
155 * signal. A spinlock, not a mutex — the critical sections are plain copies of
156 * a few hundred bytes, and every formatting step happens on a local copy after
157 * the lock is let go. */
158static portMUX_TYPE s_share_mux = portMUX_INITIALIZER_UNLOCKED;
159
160/* The Wi-Fi list handed over by the main task, and a generation counter so the
161 * page knows to refetch it after a rescan without diffing the list itself. */
163static uint16_t s_ap_count = 0U;
164/* std::atomic rather than volatile: it is incremented, and ++ on a volatile is
165 * deprecated in C++20 (and was never the atomic operation it looks like). */
166static std::atomic<uint32_t> s_scan_gen{0U};
167
168/* A value a browser has proposed. Written by the HTTP task, read and cleared by
169 * the UI task once the operator has accepted or rejected it on the panel — two
170 * tasks and a value that decides where money goes, so it takes a lock rather
171 * than a hopeful volatile. */
172static SemaphoreHandle_t s_ask_lock = NULL;
175
176/******************************************************************
177 * 4. AP identity
178 ******************************************************************/
179
181static void ap_pass_draw(void)
182{
183 /* esp_random() is the hardware RNG, and it is only a *true* one while the RF
184 * subsystem runs — prov_start() calls net_wifi_init() before coming here for
185 * exactly that reason. Which is also why bootloader_random_enable() (the SAR
186 * ADC source, for entropy with the radio off) is NOT used here: it must not be
187 * called with Wi-Fi started, and by this point it is. */
188 for (size_t i = 0; i < AP_PASS_LEN; i++) {
189 s_pass[i] = AP_PASS_ALPHABET[esp_random() % (sizeof(AP_PASS_ALPHABET) - 1U)];
190 }
191 s_pass[AP_PASS_LEN] = '\0';
192}
193
214static void ap_pass_load(void)
215{
216 nvs_handle_t h;
217 if (nvs_open(NS_PROV, NVS_READWRITE, &h) != ESP_OK) {
218 /* No NVS: a session-only passphrase is still a working portal, and the
219 * panel shows whatever this drew. */
220 ap_pass_draw();
221 return;
222 }
223
224 size_t n = sizeof(s_pass);
225 if ((nvs_get_str(h, K_AP_PASS, s_pass, &n) != ESP_OK) ||
226 (strlen(s_pass) != AP_PASS_LEN)) {
227 /* Absent, or a length this build does not issue (an older AP_PASS_LEN, or
228 * a truncated read): draw a new one and keep it. */
229 ap_pass_draw();
230 if (nvs_set_str(h, K_AP_PASS, s_pass) == ESP_OK) {
231 (void)nvs_commit(h);
232 }
233 }
234
235 /* The admin page's self-signed TLS identity, on units provisioned by an
236 * earlier build: ~1 KB of a 24 KB NVS that nothing reads any more, because the
237 * page is on this AP now and not on the venue LAN. NVS deletes logically — the
238 * entry is tombstoned and its bytes leave the page on the next compaction — so
239 * this closes the NVS read, not a raw flash dump. */
240 static const char *const DEAD[] = { K_TLS_CRT, K_TLS_KEY };
241 bool erased = false;
242 for (size_t i = 0; i < (sizeof(DEAD) / sizeof(DEAD[0])); i++) {
243 if (nvs_erase_key(h, DEAD[i]) == ESP_OK) { erased = true; }
244 }
245 if (erased) { (void)nvs_commit(h); }
246 nvs_close(h);
247}
248
250static void ap_ssid_build(void)
251{
252 uint8_t mac[6] = { 0 };
253 (void)esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
254 (void)snprintf(s_ssid, sizeof(s_ssid), "Cryptnox-%02X%02X", mac[4], mac[5]);
255}
256
257/******************************************************************
258 * 5. DNS hijack
259 ******************************************************************/
260
269static void dns_task(void *arg)
270{
271 (void)arg;
272
273 const int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
274 if (sock < 0) {
275 ESP_LOGE(TAG, "DNS socket failed");
276 s_dns_task = NULL;
277 vTaskDelete(NULL);
278 return;
279 }
280
281 struct sockaddr_in me;
282 memset(&me, 0, sizeof(me));
283 me.sin_family = AF_INET;
284 me.sin_addr.s_addr = htonl(INADDR_ANY);
285 me.sin_port = htons(53);
286 if (bind(sock, reinterpret_cast<struct sockaddr *>(&me), sizeof(me)) < 0) {
287 ESP_LOGE(TAG, "DNS bind failed");
288 close(sock);
289 s_dns_task = NULL;
290 vTaskDelete(NULL);
291 return;
292 }
293
294 /* One second, so prov_stop() is noticed promptly instead of on the next
295 * query — which on an idle AP may never come. */
296 struct timeval tv = { 1, 0 };
297 (void)setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
298
299 uint8_t buf[192];
300 while (s_dns_run) {
301 struct sockaddr_in from;
302 socklen_t from_len = sizeof(from);
303 const int n = recvfrom(sock, buf, sizeof(buf), 0,
304 reinterpret_cast<struct sockaddr *>(&from), &from_len);
305 /* 12-byte header + at least a root label and QTYPE/QCLASS. */
306 if (n < 17) { continue; }
307
308 /* Walk the QNAME label chain to find where the question ends. Bail on a
309 * compression pointer: a query has no business containing one, and
310 * following it here is how you write a loop that never returns. */
311 size_t p = 12U;
312 while ((p < static_cast<size_t>(n)) && (buf[p] != 0U)) {
313 if ((buf[p] & 0xC0U) != 0U) { p = 0U; break; }
314 p += static_cast<size_t>(buf[p]) + 1U;
315 }
316 if ((p == 0U) || ((p + 5U) > static_cast<size_t>(n))) { continue; }
317 const size_t q_end = p + 5U; /* NUL + QTYPE(2) + QCLASS(2) */
318
319 if ((q_end + 16U) > sizeof(buf)) { continue; }
320
321 buf[2] = 0x81U; /* QR=1, RD copied on: a response, recursion available */
322 buf[3] = 0x80U;
323 buf[6] = 0x00U; buf[7] = 0x01U; /* ANCOUNT = 1 */
324 buf[8] = 0x00U; buf[9] = 0x00U; /* NSCOUNT = 0 */
325 buf[10] = 0x00U; buf[11] = 0x00U; /* ARCOUNT = 0 */
326
327 size_t a = q_end;
328 buf[a++] = 0xC0U; buf[a++] = 0x0CU; /* NAME -> offset 12 */
329 buf[a++] = 0x00U; buf[a++] = 0x01U; /* TYPE A */
330 buf[a++] = 0x00U; buf[a++] = 0x01U; /* CLASS IN */
331 buf[a++] = 0x00U; buf[a++] = 0x00U;
332 buf[a++] = 0x00U; buf[a++] = 0x00U; /* TTL 0 — do not cache us */
333 buf[a++] = 0x00U; buf[a++] = 0x04U; /* RDLENGTH 4 */
334 buf[a++] = 192U; buf[a++] = 168U; buf[a++] = 4U; buf[a++] = 1U;
335
336 (void)sendto(sock, buf, a, 0,
337 reinterpret_cast<struct sockaddr *>(&from), from_len);
338 }
339
340 close(sock);
341 s_dns_task = NULL;
342 vTaskDelete(NULL);
343}
344
345/******************************************************************
346 * 6. Request helpers
347 ******************************************************************/
348
349/* form_field() lives in form_parse.h and addr_is_base58()/addr_tron_plausible()
350 * in addr_check.h, neither of which has any ESP-IDF dependency, so the host tests
351 * in tests/units can include them. Between them they are all the code here that
352 * interprets input a stranger on the network controls. */
353
355static bool read_body(httpd_req_t *req, char *out, size_t n)
356{
357 if (req->content_len >= n) { return false; }
358 size_t got = 0U;
359 while (got < req->content_len) {
360 const int r = httpd_req_recv(req, out + got, req->content_len - got);
361 if (r == HTTPD_SOCK_ERR_TIMEOUT) { continue; }
362 if (r <= 0) { return false; }
363 got += static_cast<size_t>(r);
364 }
365 out[got] = '\0';
366 return true;
367}
368
370static bool expired(void)
371{
372 return (s_deadline_us != 0) && (esp_timer_get_time() >= s_deadline_us);
373}
374
384static bool has_token(httpd_req_t *req)
385{
386 if (s_token[0] == '\0') { return false; }
387
388 char tok[TOKEN_HEX_LEN + 1U] = { 0 };
389 if (httpd_req_get_hdr_value_str(req, "X-Prov-Token", tok,
390 sizeof(tok)) != ESP_OK) {
391 return false;
392 }
393 return CW_Utils::secure_compare(reinterpret_cast<const uint8_t *>(tok),
394 reinterpret_cast<const uint8_t *>(s_token),
395 sizeof(tok));
396}
397
399static bool authed(httpd_req_t *req)
400{
401 return s_authed && has_token(req);
402}
403
405static esp_err_t reply(httpd_req_t *req, const char *status, const char *msg)
406{
407 httpd_resp_set_status(req, status);
408 httpd_resp_set_type(req, "text/plain");
409 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
410 return httpd_resp_sendstr(req, msg);
411}
412
414static esp_err_t ok(httpd_req_t *req, const char *msg)
415{
416 return reply(req, "200 OK", msg);
417}
418
421static bool uri_is(const httpd_req_t *req, const char *path)
422{
423 const size_t n = strcspn(req->uri, "?");
424 return (strlen(path) == n) && (strncmp(req->uri, path, n) == 0);
425}
426
433static bool gate(httpd_req_t *req, esp_err_t *rc)
434{
435 if (expired()) {
436 *rc = reply(req, "503 Service Unavailable",
437 "This page has closed. Reopen it on the terminal.");
438 return false;
439 }
440 /* The Wi-Fi-only portal lets a browser in without the admin code, because
441 * the terminal has lost its network and only needs a password. It opens by
442 * itself when the venue network drops — which anyone can cause — so it must
443 * not also be a way to change fees, the network, the clock or the payout
444 * without the code. Those stay behind the full admin session. */
445 if (s_wifi_only && !uri_is(req, "/api/wifi") && !uri_is(req, "/api/rescan") &&
446 !uri_is(req, "/api/scan")) {
447 *rc = reply(req, "403 Forbidden",
448 "Only the Wi-Fi network can be set from here while the "
449 "terminal is offline. Use Configure on the terminal for "
450 "anything else.");
451 return false;
452 }
453 if (!authed(req)) {
454 *rc = reply(req, "401 Unauthorized",
455 "This browser is not authorized. Enter the admin code on "
456 "the terminal screen.");
457 return false;
458 }
459 return true;
460}
461
462/******************************************************************
463 * 7. The page
464 ******************************************************************/
465
466/* The document itself is portal_page.h — PAGE_HTML and PAGE_JS. It lives next
467 * door because it is a document, not a server: ~700 lines of markup, CSS and
468 * script that only this one handler ever reads. */
469
470static esp_err_t page_get(httpd_req_t *req)
471{
472 httpd_resp_set_type(req, "text/html");
473 /* No-store, or a phone's portal browser serves a stale step back from cache
474 * after the terminal has moved on. */
475 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
476 (void)httpd_resp_sendstr_chunk(req, PAGE_HTML);
477 (void)httpd_resp_sendstr_chunk(req, PAGE_JS);
478 return httpd_resp_sendstr_chunk(req, NULL); /* end of chunked response */
479}
480
481/******************************************************************
482 * 8. API — state
483 ******************************************************************/
484
486static const char *ask_label(prov_ask_t k)
487{
488 switch (k) {
489 /* Polygon is EVM and spends this same address, and the panel is where an
490 * operator decides whether to accept it — so it says so there too, not
491 * only in the browser. */
492 case PROV_ASK_PAYOUT_ETH: return "Ethereum / Polygon payout address";
493 case PROV_ASK_PAYOUT_TRON: return "Tron payout address";
494 case PROV_ASK_CONTRACT_ETH: return "ERC-20 token contract";
495 case PROV_ASK_CONTRACT_TRON: return "TRC-20 token contract";
496 default: return "";
497 }
498}
499
500static const char *step_name(prov_step_t s)
501{
502 switch (s) {
503 case PROV_STEP_AUTH: return "auth";
504 case PROV_STEP_ADDR: return "addr";
505 case PROV_STEP_WIFI: return "wifi";
506 case PROV_STEP_DONE: return "done";
507 case PROV_STEP_ADMIN: return "admin";
508 default: return "idle";
509 }
510}
511
512/* json_escape() lives in json_out.h, next to form_parse.h and addr_check.h and for
513 * the same reason: an SSID is 32 arbitrary bytes chosen by whoever named the router,
514 * and a quote in one turns this response into something the page cannot parse. Host
515 * test: tests/units/test_json_out.cpp. */
516
517/* Defined with the proposal handlers in §10, where it belongs — the report below
518 * borrows it so the page cannot describe as usable a contract it would itself
519 * refuse if somebody pasted it in. */
520static bool addr_plausible(bool tron, const char *addr);
521
522static esp_err_t state_get(httpd_req_t *req)
523{
525 (void)prov_pending(&ask, NULL, 0U, NULL, 0U);
526
527 /* Two bodies, not one with empty fields: an unauthorised browser has no
528 * business learning the payout addresses or the venue's network name. The
529 * firmware version it can see, because the update page is useless without it
530 * and it is stamped on the About screen anyway. */
531 char body[928];
532 if (!authed(req)) {
533 (void)snprintf(body, sizeof(body),
534 "{\"mode\":\"%s\",\"step\":\"%s\",\"authed\":false,"
535 "\"auth_pending\":%s,\"version\":\"%s\"}",
536 (s_mode == PROV_MODE_WIZARD) ? "wizard" : "admin",
538 s_auth_pending ? "true" : "false",
540 } else {
541 char pay_eth[SETTINGS_PAYOUT_MAX] = "";
542 char pay_trx[SETTINGS_PAYOUT_MAX] = "";
543 char ct_eth[SETTINGS_PAYOUT_MAX] = "";
544 char ct_trx[SETTINGS_PAYOUT_MAX] = "";
545 /* Payout: the stored value only. A browser asking "who gets paid" must not
546 * be shown the compile-time fallback as though somebody had chosen it —
547 * that is exactly the confusion that leaves a terminal quietly paying an
548 * address its operator never saw, and an unset one refuses every sale.
549 * Empty means empty, and the page says "not set" because it is. */
550 if (!settings_get_payout(false, pay_eth, sizeof(pay_eth))) { pay_eth[0] = '\0'; }
551 if (!settings_get_payout(true, pay_trx, sizeof(pay_trx))) { pay_trx[0] = '\0'; }
552
553 /* Contracts: the value actually in use, plus whether an operator chose it.
554 *
555 * The same "stored only" rule was applied here and it was the wrong rule.
556 * A terminal with no stored contract is not uncontracted — it charges
557 * against the one built into the firmware, which is a real address doing a
558 * real job and is on the panel's own Tx tab. Reporting that as "not set"
559 * beside a working USDC selection reads as a fault, and the obvious repair
560 * is to paste something over a contract that was already correct.
561 *
562 * The reason the payout rule exists does not carry across: a fallback
563 * recipient is somebody else's address and the terminal refuses to spend to
564 * it, while a fallback contract is the asset the operator picked. So the
565 * page shows it and labels where it came from — which is the distinction
566 * the original comment was protecting, said out loud instead of by
567 * omission. Nothing is disclosed: the contract is in the signed image, on
568 * the panel, and public on-chain, and this body only reaches a browser that
569 * has already had the admin code typed on the terminal. */
570 const bool ct_eth_own = settings_get_contract(POS_CHAIN_ETH_USDC, ct_eth, sizeof(ct_eth));
571 const bool ct_trx_own = settings_get_contract(POS_CHAIN_TRON_USDT, ct_trx, sizeof(ct_trx));
572 /* An asset the build never configured falls back to the placeholder still
573 * sitting in config.h, which is a string and not an address. main.cpp
574 * refuses that asset over it; showing it here as the contract in use would
575 * be the page contradicting the terminal. Same plausibility test the
576 * proposals are held to, so the page cannot report as usable an address it
577 * would itself have rejected. */
578 if (!addr_plausible(false, ct_eth)) { ct_eth[0] = '\0'; }
579 if (!addr_plausible(true, ct_trx)) { ct_trx[0] = '\0'; }
580
581 char ssid[33] = "";
582 char pass[65] = "";
583 (void)settings_get_wifi(ssid, sizeof(ssid), pass, sizeof(pass));
584 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(pass), sizeof(pass));
585 char ssid_json[132] = "";
586 (void)json_escape(ssid_json, sizeof(ssid_json), ssid);
587
588 char note[sizeof(s_note)];
589 taskENTER_CRITICAL(&s_share_mux);
590 (void)CW_Utils::safe_memcpy(reinterpret_cast<uint8_t *>(note), sizeof(note),
591 reinterpret_cast<const uint8_t *>(s_note),
592 sizeof(note));
593 taskEXIT_CRITICAL(&s_share_mux);
594 note[sizeof(note) - 1U] = '\0';
595
596 (void)snprintf(body, sizeof(body),
597 "{\"mode\":\"%s\",\"step\":\"%s\",\"authed\":true,"
598 "\"auth_pending\":false,\"version\":\"%s\","
599 "\"pay_eth\":\"%s\",\"pay_trx\":\"%s\","
600 "\"ct_eth\":\"%s\",\"ct_trx\":\"%s\","
601 "\"ct_eth_own\":%s,\"ct_trx_own\":%s,"
602 "\"ssid\":\"%s\",\"pending\":\"%s\",\"note\":\"%s\","
603 "\"mainnet\":%s,\"fee_max\":%u,\"fee_prio\":%u,"
604 "\"tz_off\":%d,\"tz_dst\":%u,\"scan_gen\":%u,\"win\":%u}",
605 (s_mode == PROV_MODE_WIZARD) ? "wizard" : "admin",
607 pay_eth, pay_trx, ct_eth, ct_trx,
608 ct_eth_own ? "true" : "false",
609 ct_trx_own ? "true" : "false",
610 ssid_json, ask_label(ask), note,
611 settings_get_mainnet() ? "true" : "false",
612 static_cast<unsigned>(settings_get_max_fee_gwei()),
613 static_cast<unsigned>(settings_get_priority_fee_gwei()),
614 static_cast<int>(settings_get_tz_offset_min()),
615 static_cast<unsigned>(settings_get_tz_dst()),
616 static_cast<unsigned>(s_scan_gen.load()),
618 }
619
620 httpd_resp_set_type(req, "application/json");
621 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
622 return httpd_resp_sendstr(req, body);
623}
624
625static esp_err_t scan_get(httpd_req_t *req)
626{
627 esp_err_t rc;
628 if (!gate(req, &rc)) { return rc; }
629
630 /* Chunked: sixteen 32-character SSIDs plus their JSON overhead does not fit a
631 * stack buffer worth having on the httpd task. */
632 httpd_resp_set_type(req, "application/json");
633 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
634 (void)httpd_resp_sendstr_chunk(req, "{\"aps\":[");
635 for (uint16_t i = 0U; ; i++) {
636 net_wifi_ap_t ap;
637 bool have = false;
638 taskENTER_CRITICAL(&s_share_mux);
639 if (i < s_ap_count) { ap = s_aps[i]; have = true; }
640 taskEXIT_CRITICAL(&s_share_mux);
641 if (!have) { break; }
642 ap.ssid[sizeof(ap.ssid) - 1U] = '\0';
643 char name[132] = "";
644 (void)json_escape(name, sizeof(name), ap.ssid);
645 char one[200];
646 (void)snprintf(one, sizeof(one),
647 "%s{\"ssid\":\"%s\",\"rssi\":%d,\"open\":%s}",
648 (i == 0U) ? "" : ",", name,
649 static_cast<int>(ap.rssi),
650 ap.open ? "true" : "false");
651 (void)httpd_resp_sendstr_chunk(req, one);
652 }
653 (void)httpd_resp_sendstr_chunk(req, "]}");
654 return httpd_resp_sendstr_chunk(req, NULL);
655}
656
657/******************************************************************
658 * 9. API — authorisation
659 ******************************************************************/
660
661static esp_err_t auth_post(httpd_req_t *req)
662{
663 if (expired()) {
664 return reply(req, "503 Service Unavailable",
665 "This page has closed. Reopen it on the terminal.");
666 }
667 /* The token belongs to one browser. A reload keeps it (the page holds it in
668 * sessionStorage and sends it back), so "somebody pressed F5" still gets
669 * straight back in — but a request WITHOUT it is somebody else on this
670 * access point, and handing them the live token would let any phone that
671 * joined the Wi-Fi ride on the operator's session. */
672 if (s_authed || s_auth_pending) {
673 if (has_token(req)) { return ok(req, s_token); }
674 return reply(req, "409 Conflict",
675 s_authed ? "Another browser is already signed in. Close "
676 "the page on the terminal to start over."
677 : "Another browser is waiting for the admin code. "
678 "Cancel it on the terminal, then try again.");
679 }
680
681 /* A fresh token per session. The first four digits it maps to are shown on
682 * the panel beside the code prompt and on this browser's page, so the
683 * operator can see WHICH browser they are about to let in. */
684 for (size_t i = 0; i < TOKEN_HEX_LEN; i++) {
685 s_token[i] = "0123456789abcdef"[esp_random() & 0x0FU];
686 }
687 s_token[TOKEN_HEX_LEN] = '\0';
688
690 /* Nothing to authorise: the only reason this portal is up is that the
691 * terminal has lost its network, and whoever is asking read this AP's
692 * per-device passphrase off the panel in front of them. Only while no
693 * admin code exists, though — once one does, it is the promise that
694 * Wi-Fi sits behind it, and the AP passphrase is readable by anyone at
695 * the panel, so the code is demanded as on the full wizard. */
696 s_authed = true;
697 ESP_LOGW(TAG, "browser let in without a code (Wi-Fi-only re-join)");
698 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_NEXT, 0); }
699 return ok(req, s_token);
700 }
701
702 s_auth_pending = true;
703 ESP_LOGI(TAG, "browser asked to be authorised - admin code needed on panel");
704 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_AUTH, 0); }
705
706 return ok(req, s_token);
707}
708
709/******************************************************************
710 * 10. API — the values the panel has to confirm
711 ******************************************************************/
712
724static bool addr_plausible(bool tron, const char *addr)
725{
726 if (tron) {
727 unsigned char b[25];
728 uint8_t h[32];
729 if (!addr_tron_decode(addr, b)) { return false; }
730 if ((mbedtls_sha256(b, 21U, h, 0) != 0) ||
731 (mbedtls_sha256(h, sizeof(h), h, 0) != 0)) {
732 return false;
733 }
734 return memcmp(h, &b[21], 4U) == 0;
735 }
736 uint8_t parsed[ETH_ADDR_LEN];
737 return eth_addr_parse(addr, parsed);
738}
739
741static esp_err_t value_post(httpd_req_t *req, bool contract)
742{
743 esp_err_t rc;
744 if (!gate(req, &rc)) { return rc; }
745
746 char body[160] = { 0 };
747 if (!read_body(req, body, sizeof(body))) {
748 return reply(req, "400 Bad Request", "Bad request.");
749 }
750
751 char addr[SETTINGS_PAYOUT_MAX] = { 0 };
752 char net[8] = { 0 };
753 (void)form_field(body, "addr", addr, sizeof(addr));
754 (void)form_field(body, "net", net, sizeof(net));
755
756 const bool tron = (strcmp(net, "tron") == 0);
757 if (!addr_plausible(tron, addr)) {
758 return reply(req, "400 Bad Request", tron
759 ? "That is not a Tron address (34 characters, starts with T)."
760 : "That is not a valid Ethereum address. A mixed-case address must "
761 "carry a correct EIP-55 checksum.");
762 }
763
764 const prov_ask_t kind = contract
767
768 if (!prov_propose(kind, addr)) {
769 return reply(req, "409 Conflict",
770 "Something is already waiting to be accepted on the "
771 "terminal screen. Deal with that one first.");
772 }
773 return ok(req, "Now check that value on the terminal screen and accept it "
774 "there. It is not stored until you do.");
775}
776
777static esp_err_t payout_post(httpd_req_t *req) { return value_post(req, false); }
778static esp_err_t contract_post(httpd_req_t *req) { return value_post(req, true); }
779
790static esp_err_t fees_post(httpd_req_t *req)
791{
792 esp_err_t rc;
793 if (!gate(req, &rc)) { return rc; }
794
795 char body[96] = { 0 };
796 if (!read_body(req, body, sizeof(body))) {
797 return reply(req, "400 Bad Request", "Bad request.");
798 }
799
800 char max_s[12] = { 0 };
801 char prio_s[12] = { 0 };
802 (void)form_field(body, "max", max_s, sizeof(max_s));
803 (void)form_field(body, "prio", prio_s, sizeof(prio_s));
804
805 /* strtoul on its own answers 0 for "abc", which would then be refused as
806 * out of range anyway — but check the terminator too, so "20x" is a typo
807 * that gets reported rather than silently stored as 20. */
808 char *end_max = NULL;
809 char *end_prio = NULL;
810 const unsigned long max_gwei = strtoul(max_s, &end_max, 10);
811 const unsigned long prio_gwei = strtoul(prio_s, &end_prio, 10);
812 if ((max_s[0] == '\0') || (prio_s[0] == '\0') ||
813 (*end_max != '\0') || (*end_prio != '\0')) {
814 return reply(req, "400 Bad Request", "Both fees have to be whole numbers "
815 "of Gwei.");
816 }
817 const fee_pair_t verdict = fee_pair_check(max_gwei, prio_gwei);
818 if (verdict == FEE_PAIR_OUT_OF_RANGE) {
819 return reply(req, "400 Bad Request",
820 "Each fee has to be between 1 and 500 Gwei.");
821 }
822 if (verdict == FEE_PAIR_TIP_ABOVE_MAX) {
823 return reply(req, "400 Bad Request",
824 "The tip cannot be higher than the max fee.");
825 }
826
827 (void)settings_set_fees_gwei(static_cast<uint32_t>(max_gwei),
828 static_cast<uint32_t>(prio_gwei)); /* checked above */
829 /* The panel is very likely showing the Tx tab's two gas rows right now, with
830 * this page's card over them. Without this they stay on the old numbers until
831 * the operator leaves the settings screen and comes back. */
833 ESP_LOGI(TAG, "gas caps set from the config page: max %lu, tip %lu Gwei",
834 max_gwei, prio_gwei);
835 return ok(req, "Gas fees stored. They apply to the next sale.");
836}
837
853static esp_err_t clock_post(httpd_req_t *req)
854{
855 esp_err_t rc;
856 if (!gate(req, &rc)) { return rc; }
857
858 char body[64] = { 0 };
859 if (!read_body(req, body, sizeof(body))) {
860 return reply(req, "400 Bad Request", "Bad request.");
861 }
862
863 char off_s[12] = { 0 };
864 char dst_s[4] = { 0 };
865 (void)form_field(body, "off", off_s, sizeof(off_s));
866 (void)form_field(body, "dst", dst_s, sizeof(dst_s));
867
868 /* strtol answers 0 for "abc", which is a legitimate offset (UTC) — so the
869 * terminator is what separates "the operator picked UTC" from "that was not
870 * a number at all". */
871 char *end = NULL;
872 const long off = strtol(off_s, &end, 10);
873 if ((off_s[0] == '\0') || (*end != '\0')) {
874 return reply(req, "400 Bad Request",
875 "The offset has to be a whole number of minutes.");
876 }
877 char *dend = NULL;
878 const long dst = strtol(dst_s, &dend, 10); /* "" reads as 0: no DST */
879 if ((*dend != '\0') || !tz_dst_valid(dst) || !tz_offset_valid(off)) {
880 return reply(req, "400 Bad Request",
881 "That is not a time zone the terminal can use.");
882 }
883 (void)settings_set_tz_offset_min(static_cast<int16_t>(off));
884 (void)settings_set_tz_dst(static_cast<uint8_t>(dst));
885
886 /* The panel is very likely showing a sale screen with this page's card over
887 * it. The clock caches the offset rather than reading NVS every tick, so
888 * without this it keeps the old hour until something rebuilds the screen. */
890 ESP_LOGI(TAG, "clock set from the config page: %ld min, DST rule %ld", off, dst);
891 return ok(req, "Clock stored. The terminal's time updates in a moment.");
892}
893
914static esp_err_t network_post(httpd_req_t *req)
915{
916 esp_err_t rc;
917 if (!gate(req, &rc)) { return rc; }
918
919 char body[48] = { 0 };
920 if (!read_body(req, body, sizeof(body))) {
921 return reply(req, "400 Bad Request", "Bad request.");
922 }
923
924 char net[8] = { 0 };
925 (void)form_field(body, "net", net, sizeof(net));
926
927 const bool main_wanted = (strcmp(net, "main") == 0);
928 if (!main_wanted && (strcmp(net, "test") != 0)) {
929 return reply(req, "400 Bad Request", "Pick production or test.");
930 }
931 if (main_wanted == settings_get_mainnet()) {
932 return reply(req, "409 Conflict", "The terminal is already on that "
933 "network.");
934 }
935
936 settings_set_mainnet(main_wanted);
937 ESP_LOGW(TAG, "network switched to %s from the config page - restarting",
938 main_wanted ? "PRODUCTION" : "test");
939
940 const esp_err_t sent = ok(req, main_wanted
941 ? "Switched to the production networks. The terminal is restarting - "
942 "check the asset on its Tx tab when it comes back."
943 : "Switched to the test networks. The terminal is restarting - check the "
944 "asset on its Tx tab when it comes back.");
945 vTaskDelay(pdMS_TO_TICKS(1500));
946 esp_restart();
947 return sent; /* not reached */
948}
949
951static esp_err_t card_post(httpd_req_t *req)
952{
953 esp_err_t rc;
954 if (!gate(req, &rc)) { return rc; }
955
956 ESP_LOGI(TAG, "card-derived payout addresses requested from the page");
957 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_CARD, 0); }
958 return ok(req, "Follow the terminal screen.");
959}
960
961/******************************************************************
962 * 11. API — Wi-Fi and wizard navigation
963 ******************************************************************/
964
965static esp_err_t wifi_post(httpd_req_t *req)
966{
967 esp_err_t rc;
968 if (!gate(req, &rc)) { return rc; }
969
970 char body[256] = { 0 };
971 if (!read_body(req, body, sizeof(body))) {
972 return reply(req, "400 Bad Request", "Bad request.");
973 }
974
975 char ssid[33] = { 0 };
976 char pass[65] = { 0 };
977 (void)form_field(body, "ssid", ssid, sizeof(ssid));
978 (void)form_field(body, "pass", pass, sizeof(pass));
979 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(body), sizeof(body));
980
981 /* strlen, not what form_field returned: "ssid=%00" writes one byte and leaves
982 * a string C reads as empty, which would otherwise be staged as a network
983 * name and sent to esp_wifi_connect. See form_parse.h. */
984 if (strlen(ssid) == 0U) {
985 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(pass), sizeof(pass));
986 return reply(req, "400 Bad Request", "A network name is required.");
987 }
988
989 /* Staged into the UI's own handoff buffers and reported as the ordinary
990 * "credentials entered" event, so main's existing connect-and-verify loop —
991 * the connecting screen, the retry note, the keep-or-drop decision once the
992 * clock proves the uplink — runs unchanged. */
993 ui_stage_wifi_creds(ssid, pass);
994 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(pass), sizeof(pass));
995 ESP_LOGI(TAG, "Wi-Fi '%s' submitted from the config page", ssid);
996
997 /* Answer BEFORE the event: in wizard mode the connect attempt takes the radio
998 * down and this response would never reach the phone otherwise. One sentence
999 * for both modes — the wizard's page does not show this message at all any
1000 * more, it replaces itself with its own finished screen. */
1001 const esp_err_t sent = ok(req, "Trying that network now. Watch the terminal "
1002 "screen.");
1003 if (s_cb != NULL) { s_cb(UI_EVENT_WIFI_TRY, 0); }
1004 return sent;
1005}
1006
1007static esp_err_t rescan_post(httpd_req_t *req)
1008{
1009 esp_err_t rc;
1010 if (!gate(req, &rc)) { return rc; }
1011 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_SCAN, 0); }
1012 return ok(req, "Scanning.");
1013}
1014
1015static esp_err_t next_post(httpd_req_t *req)
1016{
1017 esp_err_t rc;
1018 if (!gate(req, &rc)) { return rc; }
1019 /* The portal does not advance its own step: main owns the order, because each
1020 * step is something main has to do (scan the radio, park on a queue) and not
1021 * just a section to reveal. */
1022 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_NEXT, 0); }
1023 return ok(req, "");
1024}
1025
1026/******************************************************************
1027 * 12. API — firmware upload
1028 ******************************************************************/
1029
1040static esp_err_t ota_post(httpd_req_t *req)
1041{
1042 esp_err_t rc;
1043 if (!gate(req, &rc)) { return rc; }
1044
1045 const char *err = "";
1046 if (!ota_begin(req->content_len, &err)) {
1047 /* One of these refusals is a dead end unless the panel is asked again: an
1048 * image already staged is only reachable through the card that
1049 * UI_EVENT_OTA_STAGED raised, staging lives in RAM, and an operator who
1050 * dismissed that card without deciding has no way back to it. So raise it
1051 * again — then "accept or discard it there first" is an instruction the
1052 * operator can actually follow. */
1053 if (ota_staged(NULL, 0U, NULL) && (s_cb != NULL)) {
1055 }
1056 return reply(req, ota_receiving() ? "409 Conflict" : "400 Bad Request", err);
1057 }
1058
1059 const size_t len = req->content_len;
1060 size_t got = 0U;
1061 unsigned stalls = 0U;
1062 bool good = true;
1063 while (good && (got < len)) {
1064 const size_t want = ((len - got) < UPLOAD_CHUNK) ? (len - got) : UPLOAD_CHUNK;
1065 const int n = httpd_req_recv(req, reinterpret_cast<char *>(s_upload),
1066 want);
1067 if (n == HTTPD_SOCK_ERR_TIMEOUT) {
1068 /* A slow uplink is not a dead one — keep waiting, but not for ever:
1069 * this loop is what the portal's deadline now waits behind. */
1070 if (++stalls >= UPLOAD_MAX_STALLS) {
1071 ESP_LOGW(TAG, "upload stalled at %u/%u bytes - giving up",
1072 static_cast<unsigned>(got), static_cast<unsigned>(len));
1073 good = false;
1074 break;
1075 }
1076 continue;
1077 }
1078 stalls = 0U;
1079 if (n <= 0) {
1080 ESP_LOGW(TAG, "upload aborted at %u/%u bytes",
1081 static_cast<unsigned>(got), static_cast<unsigned>(len));
1082 good = false;
1083 break;
1084 }
1085 good = ota_write(s_upload, static_cast<size_t>(n));
1086 got += static_cast<size_t>(n);
1087 }
1088
1089 if (!good) {
1090 ota_abort();
1091 return reply(req, "400 Bad Request",
1092 "The upload did not complete. Nothing was installed.");
1093 }
1094
1095 char ver[48] = "?";
1096 const bool ended = ota_end(ver, sizeof(ver), &err);
1097
1098 /* The one measurement that matters on this task: ota_end() has just run the
1099 * signature verification, which is this stack's high-water mark by a wide
1100 * margin (it is what used to overflow the default 4 KB). Logged either way —
1101 * a rejection verifies too. If this number ever gets close to zero, raise
1102 * cfg.stack_size in prov_start() rather than finding out from a panic. */
1103 ESP_LOGI(TAG, "httpd stack: %u bytes still free after verification",
1104 static_cast<unsigned>(uxTaskGetStackHighWaterMark(NULL)));
1105
1106 if (!ended) {
1107 return reply(req, "400 Bad Request", err);
1108 }
1109
1110 char msg[144];
1111 (void)snprintf(msg, sizeof(msg),
1112 "Version %s received and verified. Accept it on the terminal "
1113 "screen to install it and reboot.", ver);
1114 /* Answer BEFORE the event, as wifi_post() does and for the same reason: what
1115 * the event leads to is a modal on the panel whose Install button reboots this
1116 * device, and a browser that was still waiting for this line reports a
1117 * successful update as a dropped connection. The image is already staged and
1118 * ota.h's rules do not depend on the order of these two. */
1119 const esp_err_t sent = ok(req, msg);
1120 if (s_cb != NULL) { s_cb(UI_EVENT_OTA_STAGED, 0); }
1121 return sent;
1122}
1123
1124/******************************************************************
1125 * 13. Captive-portal probes
1126 ******************************************************************/
1127
1135static esp_err_t redirect(httpd_req_t *req)
1136{
1137 httpd_resp_set_status(req, "302 Found");
1138 httpd_resp_set_hdr(req, "Location", PORTAL_URL);
1139 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
1140 return httpd_resp_send(req, NULL, 0);
1141}
1142
1144static esp_err_t redirect_404(httpd_req_t *req, httpd_err_code_t err)
1145{
1146 (void)err;
1147 return redirect(req);
1148}
1149
1150/* The probe URLs worth registering explicitly. The 404 handler would catch them
1151 * all anyway; these are named because each is a specific OS's decision point and
1152 * a silent change to one is a portal that stops opening on one platform only.
1153 *
1154 * Apple's is served as 200-with-wrong-body rather than a 302: iOS follows the
1155 * redirect, compares the final body against "Success", and a body it cannot
1156 * fetch is treated as no network at all. */
1157static const char *const PROBE_URIS[] = {
1158 "/generate_204", /* Android */
1159 "/gen_204", /* Android, older */
1160 "/connecttest.txt", /* Windows NCSI */
1161 "/ncsi.txt", /* Windows NCSI */
1162 "/canonical.html", /* Firefox */
1163 "/success.txt", /* Firefox, newer */
1164 "/chat", /* some Android builds */
1165};
1166
1167static esp_err_t apple_probe(httpd_req_t *req)
1168{
1169 httpd_resp_set_type(req, "text/html");
1170 httpd_resp_set_hdr(req, "Cache-Control", "no-store");
1171 /* Deliberately NOT Apple's expected "<HTML><HEAD><TITLE>Success..." body. */
1172 return httpd_resp_sendstr(req,
1173 "<HTML><HEAD><TITLE>Setup</TITLE></HEAD>"
1174 "<BODY><A href='" PORTAL_URL "'>Cryptnox setup</A></BODY></HTML>");
1175}
1176
1177/******************************************************************
1178 * 14. Handler registration
1179 ******************************************************************/
1180
1181static void register_handlers(void)
1182{
1183 const httpd_uri_t api[] = {
1184 { "/", HTTP_GET, page_get, NULL },
1185 { "/api/state", HTTP_GET, state_get, NULL },
1186 { "/api/scan", HTTP_GET, scan_get, NULL },
1187 { "/api/auth", HTTP_POST, auth_post, NULL },
1188 { "/api/payout", HTTP_POST, payout_post, NULL },
1189 { "/api/contract", HTTP_POST, contract_post, NULL },
1190 { "/api/fees", HTTP_POST, fees_post, NULL },
1191 { "/api/clock", HTTP_POST, clock_post, NULL },
1192 { "/api/network", HTTP_POST, network_post, NULL },
1193 { "/api/card", HTTP_POST, card_post, NULL },
1194 { "/api/wifi", HTTP_POST, wifi_post, NULL },
1195 { "/api/rescan", HTTP_POST, rescan_post, NULL },
1196 { "/api/next", HTTP_POST, next_post, NULL },
1197 { "/api/ota", HTTP_POST, ota_post, NULL },
1198 };
1199 for (size_t i = 0; i < (sizeof(api) / sizeof(api[0])); i++) {
1200 (void)httpd_register_uri_handler(s_httpd, &api[i]);
1201 }
1202
1203 /* Both modes: they are the same SoftAP with the same page on it, so the admin
1204 * page opens itself on the phone exactly like the wizard's does. */
1205 for (size_t i = 0; i < (sizeof(PROBE_URIS) / sizeof(PROBE_URIS[0])); i++) {
1206 const httpd_uri_t p = { PROBE_URIS[i], HTTP_GET, redirect, NULL };
1207 (void)httpd_register_uri_handler(s_httpd, &p);
1208 }
1209 const httpd_uri_t a1 = { "/hotspot-detect.html", HTTP_GET, apple_probe, NULL };
1210 const httpd_uri_t a2 = { "/library/test/success.html", HTTP_GET, apple_probe, NULL };
1211 (void)httpd_register_uri_handler(s_httpd, &a1);
1212 (void)httpd_register_uri_handler(s_httpd, &a2);
1213
1214 (void)httpd_register_err_handler(s_httpd, HTTPD_404_NOT_FOUND, redirect_404);
1215}
1216
1217/******************************************************************
1218 * 15. Public API
1219 ******************************************************************/
1220
1222{
1223 if (s_mode == mode) {
1224 /* Idempotent, but restart the clock: the operator asked again. */
1225 if (mode == PROV_MODE_ADMIN) {
1226 s_deadline_us = esp_timer_get_time() +
1227 ((int64_t)PROV_WINDOW_MIN * 60LL * 1000000LL);
1228 }
1229 return s_httpd != NULL;
1230 }
1231 if (s_mode != PROV_MODE_OFF) {
1232 ESP_LOGE(TAG, "portal already up in mode %d - stop it first",
1233 static_cast<int>(s_mode.load()));
1234 return false;
1235 }
1236
1237 if (s_ask_lock == NULL) {
1238 s_ask_lock = xSemaphoreCreateMutex();
1239 if (s_ask_lock == NULL) { return false; }
1240 }
1241 s_cb = cb;
1242 s_authed = false;
1243 s_auth_pending = false;
1244 s_wifi_only = false;
1245 s_token[0] = '\0';
1246
1247 const bool wizard = (mode == PROV_MODE_WIZARD);
1248
1249 /* Both modes are the same SoftAP with the same page on it. Not a convenience:
1250 * httpd binds every interface, so a portal running beside a station
1251 * association answers the venue LAN too — and every device holding the venue
1252 * PSK is on that LAN. The AP is the perimeter, so it has to be the only
1253 * interface there is. net_ap_start() drops the station for exactly that
1254 * reason, and net_ap_stop() puts it back. */
1255
1256 /* Before the passphrase, not after: on the first portal this device ever opens
1257 * there is one to draw, and esp_random() is only properly seeded once the RF
1258 * subsystem is running — net_wifi_init() is what starts it. Drawing first
1259 * would take the bootloader's entropy, which is the one thing this AP relies
1260 * on. Every portal after that reads the stored one. */
1261 net_wifi_init();
1262 ap_ssid_build();
1263 ap_pass_load(); /* drawn once, kept until a factory reset */
1264 (void)snprintf(s_qr, sizeof(s_qr), "WIFI:T:WPA;S:%s;P:%s;;", s_ssid, s_pass);
1265
1266 if (!net_ap_start(s_ssid, s_pass)) {
1267 ESP_LOGE(TAG, "SoftAP failed to start");
1268 /* prov_stop() never runs for a portal that never came up, so the
1269 * passphrase it would have wiped is wiped here instead. */
1270 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_pass), sizeof(s_pass));
1271 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_qr), sizeof(s_qr));
1272 return false;
1273 }
1274
1275 httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
1276 cfg.max_uri_handlers = 24;
1277
1278 /* 16 KB, not the default 4 KB, and the firmware upload is the only reason.
1279 *
1280 * esp_ota_end() verifies the image it has just written, and on a Secure Boot
1281 * build that means an RSA-3072 PSS verification inside mbedTLS — which needs
1282 * several KB of stack and runs on whichever task called it. That is this one,
1283 * the httpd task serving /api/ota, so a completed upload ended in
1284 *
1285 * secure_boot_v2: Verifying with RSA-PSS...
1286 * ***ERROR*** A stack overflow in task httpd has been detected.
1287 *
1288 * and a reboot: the file arrived, the signature was genuine, and the terminal
1289 * panicked between the two every single time. Nothing was ever staged, and from
1290 * the browser it looked like the connection dropped, because it had.
1291 *
1292 * 16 KB is the size the UI task already runs at, and it does the same
1293 * verification from esp_ota_set_boot_partition() when Install is tapped.
1294 *
1295 * Measured, not guessed: a 1.0.2 image verified with 11684 of these 16384 bytes
1296 * still free, so the peak is ~4.7 KB and the old default was about 600 bytes
1297 * short — which is why it failed every time rather than occasionally. Do not
1298 * trim this to the measurement: mbedTLS's RSA path is the tallest thing either
1299 * task does, and ~11 KB of headroom on a transient server is cheaper than
1300 * another panic between a verified image and the operator's screen. ota_post
1301 * logs the figure after every upload if it ever needs checking again. */
1302 cfg.stack_size = 16384;
1303
1304 /* The upload is one request that holds a socket for minutes, and LRU purge
1305 * picks the least recently used socket — which is exactly that one, since its
1306 * request began before every poll and captive-portal probe that followed. A PC
1307 * is the case that shows it: Windows probes for internet the whole time it is on
1308 * an AP that has none, and each probe is another connection. First the socket
1309 * table ran dry (87 x "error in accept (23)" = ENFILE in one session), then the
1310 * upload was sacrificed for the next probe and stalled at 33%.
1311 *
1312 * So: no purging, and TCP keepalive to reap what purging used to. A dead peer is
1313 * dropped in ~20s, a live upload is never dropped, and a probe that finds the
1314 * table full is refused — which costs a Windows machine nothing it was going to
1315 * get anyway. The page also stops its 1.5s poll while sending (PAGE_JS). */
1316 cfg.lru_purge_enable = false;
1317 cfg.keep_alive_enable = true;
1318 cfg.keep_alive_idle = 5;
1319 cfg.keep_alive_interval = 5;
1320 cfg.keep_alive_count = 3;
1321 /* Port 80 is not optional: a captive-portal probe fetches a bare http:// URL
1322 * and will not follow us anywhere else. Plain HTTP over WPA2 is the whole
1323 * transport story now — see provision.h. */
1324 cfg.server_port = 80;
1325 /* A firmware image is 1.9 MB and each erase-and-write pause inside the
1326 * upload is seconds. The default 5 s would drop the socket mid-image;
1327 * ota_post() also retries on timeout, and between the two a slow phone
1328 * survives. */
1329 cfg.recv_wait_timeout = 30;
1330 cfg.send_wait_timeout = 30;
1331 if (httpd_start(&s_httpd, &cfg) != ESP_OK) {
1332 ESP_LOGE(TAG, "httpd failed to start");
1333 s_httpd = NULL;
1334 net_ap_stop();
1335 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_pass), sizeof(s_pass));
1336 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_qr), sizeof(s_qr));
1337 return false;
1338 }
1339
1341
1342 s_mode = mode;
1344
1345 /* No self-close for the wizard: a terminal halfway through setup that shut its
1346 * own setup page after a quarter of an hour would strand whoever went to fetch
1347 * the Wi-Fi password. main stops this one when setup ends. Admin mode does
1348 * close itself — it has taken a working terminal off its network to do this. */
1349 s_deadline_us = wizard ? 0
1350 : (esp_timer_get_time() +
1351 ((int64_t)PROV_WINDOW_MIN * 60LL * 1000000LL));
1352
1353 s_dns_run = true;
1354 if (xTaskCreate(dns_task, "prov_dns", 3072, NULL, 4, &s_dns_task) != pdPASS) {
1355 /* The forms still work for anyone who types the IP, but the portal will
1356 * not open by itself — which is the entire point, so say so. */
1357 ESP_LOGE(TAG, "DNS task failed - captive portal will NOT auto-open");
1358 s_dns_run = false;
1359 s_dns_task = NULL;
1360 }
1361 ESP_LOGW(TAG, "%s portal up: SSID '%s', pass '%s', %s",
1362 wizard ? "setup" : "admin", s_ssid, s_pass, PORTAL_URL);
1363 /* The tight moment for heap on this board: httpd, its sockets and the DNS task
1364 * are now up alongside LVGL and the Wi-Fi driver, and a firmware upload is
1365 * about to ask for buffers on top. The largest free block, not just the total —
1366 * a TLS or upload allocation fails on fragmentation long before the sum runs
1367 * out, and without a number a field failure cannot be attributed at all. */
1368 ESP_LOGI(TAG, "heap after portal start: %u free, %u largest block",
1369 (unsigned)esp_get_free_heap_size(),
1370 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
1371 return true;
1372}
1373
1374void prov_stop(void)
1375{
1376 /* Claim the teardown. Whoever gets a non-OFF value here owns it; anyone else
1377 * arriving concurrently sees OFF and returns. */
1378 const prov_mode_t was = s_mode.exchange(PROV_MODE_OFF);
1379 if (was == PROV_MODE_OFF) { return; }
1380
1382
1383 s_dns_run = false;
1384 /* The task closes its socket and deletes itself within one recv timeout. */
1385 for (int i = 0; (i < 20) && (s_dns_task != NULL); i++) {
1386 vTaskDelay(pdMS_TO_TICKS(100));
1387 }
1388
1389 if (s_httpd != NULL) {
1390 (void)httpd_stop(s_httpd);
1391 s_httpd = NULL;
1392 }
1393 /* Both modes raise the AP, and net_ap_stop() also re-joins the network the AP
1394 * displaced — so closing the admin page puts a working terminal back online. */
1395 net_ap_stop();
1396
1397 /* An upload half-received does not outlive the page it arrived through.
1398 *
1399 * An image that is already *staged* does, and that is deliberate: it has been
1400 * verified, it is on the panel waiting for somebody to accept it, and
1401 * installing it needs no page at all. This used to call ota_forget() here, and
1402 * it was the bug that made "the update did not install" reproducible — a
1403 * 1.9 MB upload over the SoftAP eats minutes of the 15-minute admin window, so
1404 * the window routinely expired while the operator was reading the Install
1405 * button, prov_stop() withdrew the offer from under them, and the tap then did
1406 * nothing at all. Nothing about that made the device safer: an image can only
1407 * be here at all if it carries a signature from the key this firmware trusts,
1408 * and the panel is still the only thing that can install it. Discard on the
1409 * panel is what drops a staged image (ota_commit(false)). */
1410 ota_abort();
1411
1412 s_authed = false;
1413 s_auth_pending = false;
1414 s_wifi_only = false;
1415 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_token), sizeof(s_token));
1416
1417 /* The AP passphrase too: the AP it opened is down, the next portal reads it
1418 * back from NVS, and the QR screen is only ever painted while a portal is up —
1419 * so nothing needs it in RAM in between. A proposed value IS dropped as well:
1420 * unaccepted means unwanted. */
1421 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_pass), sizeof(s_pass));
1422 if (s_ask_lock != NULL) {
1423 if (xSemaphoreTake(s_ask_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
1425 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_ask_val),
1426 sizeof(s_ask_val));
1427 (void)xSemaphoreGive(s_ask_lock);
1428 }
1429 }
1430 /* s_qr holds the passphrase, so clear all of it, not just byte 0. */
1431 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_qr), sizeof(s_qr));
1432 s_deadline_us = 0;
1433 ESP_LOGI(TAG, "config portal down");
1434}
1435
1437
1438void prov_set_step(prov_step_t step) { s_step = step; }
1439
1441
1443{
1444 if ((s_httpd == NULL) || (s_deadline_us == 0)) { return 0U; }
1445 const int64_t left = s_deadline_us - esp_timer_get_time();
1446 if (left <= 0) { return 0U; }
1447 /* Round up: "1 min left" should not read as 0 for the last 59 seconds. */
1448 return static_cast<unsigned>((left + (59LL * 1000000LL)) / 60000000LL);
1449}
1450
1451const char *prov_ap_ssid(void) { return s_ssid; }
1452const char *prov_ap_pass(void) { return s_pass; }
1453const char *prov_qr_payload(void) { return s_qr; }
1454
1455bool prov_auth_pending(void) { return s_auth_pending; }
1456
1457void prov_auth_resolve(bool grant)
1458{
1459 if (!s_auth_pending) { return; }
1460 s_auth_pending = false;
1461 s_authed = grant;
1462 if (grant) {
1463 ESP_LOGW(TAG, "browser authorised from the panel");
1464 /* Reported as "move on", because that is what it is: nothing else happens
1465 * when a browser is let in, and the wizard is parked on its queue waiting
1466 * for something to happen. Without this the code would be accepted and the
1467 * flow would sit on the authorise step until somebody pressed Continue. */
1468 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_NEXT, 0); }
1469 } else {
1470 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_token), sizeof(s_token));
1471 ESP_LOGW(TAG, "browser authorisation refused");
1472 }
1473}
1474
1475bool prov_authed(void) { return s_authed; }
1476
1477bool prov_pair_code(char *out, size_t out_size)
1478{
1479 if ((out == NULL) || (out_size < 5U) || (s_token[0] == '\0')) { return false; }
1480 unsigned v = 0U;
1481 for (size_t i = 0U; i < 4U; i++) {
1482 const char c = s_token[i];
1483 v = (v << 4) | static_cast<unsigned>((c <= '9') ? (c - '0') : (c - 'a' + 10));
1484 }
1485 (void)snprintf(out, out_size, "%04u", v % 10000U);
1486 return true;
1487}
1488
1489void prov_set_wifi_only(void) { s_wifi_only = true; }
1490
1491bool prov_wifi_only(void) { return s_wifi_only; }
1492
1493void prov_set_note(const char *note)
1494{
1495 /* Sanitised rather than JSON-escaped. Every note is one of this firmware's own
1496 * sentences, so there is nothing to escape today — but it lands in a JSON
1497 * string literal, and a future caller pasting an SSID or an error string in
1498 * here should get a dropped character rather than a response the page cannot
1499 * parse. */
1500 char clean[sizeof(s_note)];
1501 size_t w = 0U;
1502 if (note != NULL) {
1503 for (const char *p = note; (*p != '\0') && (w < (sizeof(clean) - 1U)); p++) {
1504 const unsigned char c = static_cast<unsigned char>(*p);
1505 if ((c == '"') || (c == '\\') || (c < 0x20U)) { continue; }
1506 clean[w++] = static_cast<char>(c);
1507 }
1508 }
1509 clean[w] = '\0';
1510 taskENTER_CRITICAL(&s_share_mux);
1511 (void)CW_Utils::safe_memcpy(reinterpret_cast<uint8_t *>(s_note), sizeof(s_note),
1512 reinterpret_cast<const uint8_t *>(clean), w + 1U);
1513 taskEXIT_CRITICAL(&s_share_mux);
1514}
1515
1516void prov_set_scan(const net_wifi_ap_t *aps, uint16_t n)
1517{
1518 if (aps == NULL) { n = 0U; }
1519 if (n > PROV_MAX_APS) { n = PROV_MAX_APS; }
1520 taskENTER_CRITICAL(&s_share_mux);
1521 for (uint16_t i = 0U; i < n; i++) { s_aps[i] = aps[i]; }
1522 s_ap_count = n;
1523 taskEXIT_CRITICAL(&s_share_mux);
1524 s_scan_gen++; /* the page refetches when this moves */
1525}
1526
1527bool prov_propose(prov_ask_t kind, const char *addr)
1528{
1529 if ((s_ask_lock == NULL) || (addr == NULL) || (kind == PROV_ASK_NONE)) {
1530 return false;
1531 }
1532 if (xSemaphoreTake(s_ask_lock, pdMS_TO_TICKS(500)) != pdTRUE) { return false; }
1533 if (s_ask != PROV_ASK_NONE) {
1534 (void)xSemaphoreGive(s_ask_lock);
1535 return false;
1536 }
1537 s_ask = kind;
1538 (void)snprintf(s_ask_val, sizeof(s_ask_val), "%s", addr);
1539 (void)xSemaphoreGive(s_ask_lock);
1540
1541 ESP_LOGW(TAG, "%s proposed: %s - awaiting on-screen accept",
1542 ask_label(kind), addr);
1543 if (s_cb != NULL) { s_cb(UI_EVENT_PROV_VALUE, 0); }
1544 return true;
1545}
1546
1547bool prov_pending(prov_ask_t *kind, char *label, size_t label_n,
1548 char *value, size_t value_n)
1549{
1550 if (s_ask_lock == NULL) { return false; }
1551 if (xSemaphoreTake(s_ask_lock, pdMS_TO_TICKS(100)) != pdTRUE) { return false; }
1552
1553 const bool waiting = (s_ask != PROV_ASK_NONE);
1554 if (waiting) {
1555 if (kind != NULL) { *kind = s_ask; }
1556 if ((label != NULL) && (label_n > 0U)) {
1557 (void)snprintf(label, label_n, "%s", ask_label(s_ask));
1558 }
1559 if ((value != NULL) && (value_n > 0U)) {
1560 (void)snprintf(value, value_n, "%s", s_ask_val);
1561 }
1562 } else if (kind != NULL) {
1563 *kind = PROV_ASK_NONE;
1564 }
1565 (void)xSemaphoreGive(s_ask_lock);
1566 return waiting;
1567}
1568
1569bool prov_pending_commit(bool accept)
1570{
1571 if (s_ask_lock == NULL) { return false; }
1572 if (xSemaphoreTake(s_ask_lock, pdMS_TO_TICKS(100)) != pdTRUE) { return false; }
1573
1574 bool stored = false;
1575 if ((s_ask != PROV_ASK_NONE) && accept) {
1576 switch (s_ask) {
1578 stored = settings_set_payout(false, s_ask_val); break;
1580 stored = settings_set_payout(true, s_ask_val); break;
1585 default: break;
1586 }
1587 }
1588 const bool had = (s_ask != PROV_ASK_NONE);
1590 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_ask_val), sizeof(s_ask_val));
1591 (void)xSemaphoreGive(s_ask_lock);
1592
1593 if (s_cb != NULL) {
1594 if (stored) { s_cb(UI_EVENT_PROV_VALUE_SET, 0); }
1595 else if (had) { s_cb(UI_EVENT_PROV_VALUE_NO, 0); }
1596 }
1597 return stored;
1598}
Structural address checks for the config portal.
static bool addr_tron_decode(const char *s, unsigned char out[25])
Decode a base58 Tron address into its 25 bytes (0x41 || 20-byte key hash || 4-byte checksum).
Definition addr_check.h:66
TZ-independent calendar arithmetic and date-string parsing.
bool eth_addr_parse(const char *hex, uint8_t out[ETH_ADDR_LEN])
Parse a 20-byte Ethereum address from a hex string.
Definition eth_addr.cpp:34
Hex Ethereum-address parsing — a pure, dependency-free unit so it can be unit-tested and fuzzed on th...
#define ETH_ADDR_LEN
Length of a raw (binary) Ethereum address, in bytes.
Definition eth_addr.h:25
static const char *const TAG
Definition eth_rpc.cpp:33
application/x-www-form-urlencoded field extraction.
static size_t form_field(const char *body, const char *key, char *out, size_t n)
Pull one field out of a urlencoded body, percent-decoding it.
Definition form_parse.h:46
Escaping for values the config portal puts inside a JSON string.
static size_t json_escape(char *out, size_t n, const char *val)
Copy val into out, escaping what JSON requires.
Definition json_out.h:46
bool net_ap_start(const char *ssid, const char *pass)
Raise a WPA2 SoftAP as the radio's only interface, for phone-based configuration.
Definition net.cpp:215
void net_ap_stop(void)
Drop the SoftAP, return the radio to station-only, and re-join.
Definition net.cpp:286
void net_wifi_init(void)
Bring up the WiFi driver in station mode (idempotent).
Definition net.cpp:175
Network bring-up: Wi-Fi station (init/scan/connect/RSSI) and SNTP time sync. No application-protocol ...
void ota_abort(void)
Give up on an upload in progress. Nothing is installed. Safe always.
Definition ota.cpp:156
const char * ota_running_version(void)
The running firmware's version, from the image header.
Definition ota.cpp:263
bool ota_end(char *ver_out, size_t ver_n, const char **err)
Close and verify the received image, then stage it for the panel.
Definition ota.cpp:167
bool ota_receiving(void)
Whether an upload is in flight, so a second can be refused.
Definition ota.cpp:165
bool ota_staged(char *version, size_t version_n, bool *older)
Fetch the version of an image that has been received and verified but not yet installed.
Definition ota.cpp:275
bool ota_write(const void *buf, size_t n)
Append n bytes to the open slot.
Definition ota.cpp:145
bool ota_begin(size_t len, const char **err)
Open the idle slot for an image of len bytes.
Definition ota.cpp:94
Firmware slot handling: receive an image into the idle slot, verify it, and install it only once some...
token_t s_token[POS_CHAIN__COUNT]
Definition pay.cpp:30
The config portal's document — the HTML/CSS half and the script half.
static const char *const PAGE_HTML
Definition portal_page.h:40
static const char *const PAGE_JS
#define PORTAL_URL
Definition portal_page.h:34
static net_wifi_ap_t s_aps[PROV_MAX_APS]
static std::atomic< uint32_t > s_scan_gen
static esp_err_t card_post(httpd_req_t *req)
The browser asks the terminal to read the addresses off a card.
static esp_err_t rescan_post(httpd_req_t *req)
#define PROV_MAX_APS
void prov_auth_resolve(bool grant)
Answer a pending authorisation request from the panel.
bool prov_pair_code(char *out, size_t out_size)
The 4-digit pairing code of the browser asking to be let in.
static esp_err_t wifi_post(httpd_req_t *req)
static char s_pass[AP_PASS_LEN+1U]
static const char *const PROBE_URIS[]
static esp_err_t clock_post(httpd_req_t *req)
Store the panel clock's standard offset from UTC and its DST rule.
void prov_set_step(prov_step_t step)
Tell the portal which wizard step is current.
static void ap_ssid_build(void)
SSID from the SoftAP MAC, so two terminals in a room are tellable apart.
static uint16_t s_ap_count
static const char AP_PASS_ALPHABET[]
Definition provision.cpp:70
static bool expired(void)
True once the portal's own window has closed.
static const char * ask_label(prov_ask_t k)
Label the panel and the page both use for a pending proposal.
static esp_err_t next_post(httpd_req_t *req)
#define NS_PROV
Definition provision.cpp:75
static prov_ask_t s_ask
prov_step_t prov_step(void)
The current step.
static bool authed(httpd_req_t *req)
Whether the request is the browser that was let in.
const char * prov_ap_pass(void)
AP passphrase, or "" while no portal is up.
void prov_set_note(const char *note)
Put a one-line message on the page.
static volatile bool s_dns_run
static uint8_t s_upload[UPLOAD_CHUNK]
static char s_ask_val[SETTINGS_PAYOUT_MAX]
static volatile prov_step_t s_step
static esp_err_t value_post(httpd_req_t *req, bool contract)
Shared body of /api/payout and /api/contract.
static SemaphoreHandle_t s_ask_lock
static esp_err_t apple_probe(httpd_req_t *req)
static esp_err_t ota_post(httpd_req_t *req)
Stream a firmware image into the idle slot, without booting it.
bool prov_pending_commit(bool accept)
Resolve a pending proposal from the panel.
static TaskHandle_t s_dns_task
static void ap_pass_load(void)
The AP passphrase: drawn once, then kept until a factory reset.
static void register_handlers(void)
#define UPLOAD_MAX_STALLS
static bool uri_is(const httpd_req_t *req, const char *path)
Whether the request's path is path — req->uri carries any query string, which routing ignores and thi...
void prov_set_wifi_only(void)
Cut the wizard down to the Wi-Fi step, with no admin code.
bool prov_wifi_only(void)
Whether the portal is the cut-down Wi-Fi-only flow.
static void dns_task(void *arg)
Answer every A query with the portal address.
const char * prov_qr_payload(void)
What the panel's QR code should carry.
const char * prov_ap_ssid(void)
AP SSID, or "" while no portal is up.
bool prov_propose(prov_ask_t kind, const char *addr)
Propose a value on behalf of the panel itself.
static ui_event_cb_t s_cb
static esp_err_t redirect_404(httpd_req_t *req, httpd_err_code_t err)
404 handler — the catch-all for probe URLs not listed below.
static esp_err_t auth_post(httpd_req_t *req)
#define UPLOAD_CHUNK
Definition provision.cpp:94
#define K_TLS_KEY
Definition provision.cpp:82
#define K_TLS_CRT
Definition provision.cpp:81
static esp_err_t ok(httpd_req_t *req, const char *msg)
200 with a plain-text message.
static portMUX_TYPE s_share_mux
static esp_err_t redirect(httpd_req_t *req)
Send every probe and stray URL to the portal.
bool prov_authed(void)
Whether the browser session is authorised to change anything.
static esp_err_t fees_post(httpd_req_t *req)
Store the EIP-1559 gas caps (Gwei).
static const char * step_name(prov_step_t s)
static esp_err_t contract_post(httpd_req_t *req)
static int64_t s_deadline_us
bool prov_auth_pending(void)
Whether a browser has asked to be authorised.
static volatile bool s_wifi_only
static volatile bool s_auth_pending
prov_mode_t prov_mode(void)
Which mode is running, or PROV_MODE_OFF.
void prov_stop(void)
Stop the portal, drop the AP, and withdraw anything unaccepted.
static bool has_token(httpd_req_t *req)
Whether the request carries the token of the authorised session.
#define TOKEN_HEX_LEN
static esp_err_t reply(httpd_req_t *req, const char *status, const char *msg)
Send a plain-text status line; the page shows it verbatim.
static void ap_pass_draw(void)
Fill s_pass with AP_PASS_LEN characters of hardware entropy.
static char s_qr[96]
static bool read_body(httpd_req_t *req, char *out, size_t n)
Read a request body into out.
#define K_AP_PASS
Definition provision.cpp:78
static esp_err_t payout_post(httpd_req_t *req)
static httpd_handle_t s_httpd
void prov_set_scan(const net_wifi_ap_t *aps, uint16_t n)
Hand the portal a Wi-Fi scan for the browser to choose from.
static bool gate(httpd_req_t *req, esp_err_t *rc)
The gate every mutating endpoint runs first.
static volatile bool s_authed
static char s_note[128]
static std::atomic< prov_mode_t > s_mode
static esp_err_t scan_get(httpd_req_t *req)
static esp_err_t page_get(httpd_req_t *req)
static char s_ssid[33]
bool prov_pending(prov_ask_t *kind, char *label, size_t label_n, char *value, size_t value_n)
Fetch the value a browser proposed but nobody has accepted.
bool prov_start(prov_mode_t mode, ui_event_cb_t cb)
Raise the portal.
#define AP_PASS_LEN
Definition provision.cpp:66
static esp_err_t state_get(httpd_req_t *req)
static esp_err_t network_post(httpd_req_t *req)
Switch the terminal between the production and the test networks.
unsigned prov_window_left_min(void)
Minutes left before the portal closes itself, 0 once it has.
static bool addr_plausible(bool tron, const char *addr)
Reject an address that is obviously not one, before it is proposed.
The config portal: one web app for setting a terminal up and for administering it afterwards,...
prov_ask_t
What the panel is being asked to accept, for prov_pending.
Definition provision.h:128
@ PROV_ASK_PAYOUT_TRON
Definition provision.h:131
@ PROV_ASK_CONTRACT_ETH
Definition provision.h:132
@ PROV_ASK_PAYOUT_ETH
Definition provision.h:130
@ PROV_ASK_CONTRACT_TRON
Definition provision.h:133
@ PROV_ASK_NONE
Definition provision.h:129
prov_step_t
Where the wizard has got to.
Definition provision.h:118
@ PROV_STEP_WIFI
Definition provision.h:122
@ PROV_STEP_AUTH
Definition provision.h:120
@ PROV_STEP_IDLE
Definition provision.h:119
@ PROV_STEP_ADMIN
Definition provision.h:124
@ PROV_STEP_ADDR
Definition provision.h:121
@ PROV_STEP_DONE
Definition provision.h:123
#define PROV_WINDOW_MIN
How long the portal stays up before closing itself, minutes.
Definition provision.h:101
prov_mode_t
Which of the two portals is running.
Definition provision.h:104
@ PROV_MODE_WIZARD
Definition provision.h:106
@ PROV_MODE_OFF
Definition provision.h:105
@ PROV_MODE_ADMIN
Definition provision.h:107
bool settings_get_wifi(char *ssid, size_t ssid_n, char *pass, size_t pass_n)
Read the stored Wi-Fi credentials.
Definition settings.cpp:298
bool settings_set_tz_offset_min(int16_t minutes)
Store the panel clock's UTC offset.
Definition settings.cpp:234
bool settings_get_contract(pos_chain_t chain, char *out, size_t n)
Read the token-contract address for a network.
Definition settings.cpp:549
int16_t settings_get_tz_offset_min(void)
The panel clock's standard (winter) offset from UTC, in minutes east.
Definition settings.cpp:226
uint8_t settings_get_tz_dst(void)
The clock's DST rule, a civil_dst_t; CIVIL_DST_NONE when unset.
Definition settings.cpp:243
uint32_t settings_get_priority_fee_gwei(void)
EIP-1559 max priority fee (tip) per gas, in Gwei.
Definition settings.cpp:352
bool settings_set_fees_gwei(uint32_t max_gwei, uint32_t prio_gwei)
Persist the max fee and the tip per gas (Gwei), as a pair.
Definition settings.cpp:357
bool settings_get_payout(bool tron, char *out, size_t n)
Read the payout address for a network.
Definition settings.cpp:525
void settings_set_mainnet(bool mainnet)
Persist the production/test network choice.
Definition settings.cpp:201
bool settings_set_contract(pos_chain_t chain, const char *addr)
Persist a token-contract address, value and echo copy.
Definition settings.cpp:566
bool settings_set_payout(bool tron, const char *addr)
Persist a payout address, writing both the value and its echo copy.
Definition settings.cpp:542
bool settings_set_tz_dst(uint8_t rule)
Store the DST rule.
Definition settings.cpp:248
bool settings_has_admin_code(void)
true once an admin code exists.
Definition settings.cpp:372
bool settings_get_mainnet(void)
true when the terminal is on the production networks.
Definition settings.cpp:189
uint32_t settings_get_max_fee_gwei(void)
EIP-1559 max fee per gas, in Gwei.
Definition settings.cpp:347
Persistent device settings stored in NVS (backlight, Wi-Fi creds).
@ POS_CHAIN_ETH_USDC
Definition settings.h:34
@ POS_CHAIN_TRON_USDT
Definition settings.h:36
#define SETTINGS_PAYOUT_MAX
Longest payout address plus NUL — "0x" + 40 hex, or 34 base58 Tron.
Definition settings.h:214
What a setting may be, apart from where it is stored.
fee_pair_t
Verdict on a (max fee, tip) pair from the config page.
@ FEE_PAIR_OUT_OF_RANGE
@ FEE_PAIR_TIP_ABOVE_MAX
static bool tz_dst_valid(long rule)
true for a DST rule the clock knows.
static bool tz_offset_valid(long minutes)
true for an offset the clock accepts (TZ_OFFSET_MIN .. _MAX).
static fee_pair_t fee_pair_check(unsigned long max_gwei, unsigned long prio_gwei)
Check a (max fee, tip) pair in Gwei; the range is checked first.
A scanned access point (subset of fields the UI needs).
Definition net.h:33
char ssid[33]
Definition net.h:34
bool open
Definition net.h:36
int8_t rssi
Definition net.h:35
void ui_fees_changed(void)
Note that the stored gas caps have changed, so the Tx tab can catch up.
Definition ui.cpp:783
void ui_stage_wifi_creds(const char *ssid, const char *pass)
Load credentials into the same handoff buffers the picker fills.
Definition ui.cpp:842
void ui_clock_changed(void)
Note that the stored UTC offset has changed, so the clock can catch up.
Definition ui.cpp:788
@ UI_EVENT_OTA_STAGED
Definition ui.h:82
@ UI_EVENT_PROV_AUTH
Definition ui.h:66
@ UI_EVENT_WIFI_TRY
Definition ui.h:61
@ UI_EVENT_PROV_VALUE_NO
Definition ui.h:71
@ UI_EVENT_PROV_CARD
Definition ui.h:75
@ UI_EVENT_PROV_VALUE
Definition ui.h:68
@ UI_EVENT_PROV_SCAN
Definition ui.h:77
@ UI_EVENT_PROV_NEXT
Definition ui.h:78
@ UI_EVENT_PROV_VALUE_SET
Definition ui.h:70
void(* ui_event_cb_t)(ui_event_t event, uint64_t payload)
Callback invoked from the UI task on user interaction.
Definition ui.h:128