23#include "freertos/FreeRTOS.h"
24#include "freertos/task.h"
25#include "freertos/semphr.h"
33#include "lwip/sockets.h"
35#include "esp_heap_caps.h"
36#include "esp_http_server.h"
39#include "esp_random.h"
40#include "esp_system.h"
43#include "mbedtls/sha256.h"
56static const char *
const TAG =
"prov";
66#define AP_PASS_LEN 10U
78#define K_AP_PASS "ap_pass"
81#define K_TLS_CRT "tls_crt"
82#define K_TLS_KEY "tls_key"
94#define UPLOAD_CHUNK 4096U
101#define UPLOAD_MAX_STALLS 4U
103#define TOKEN_HEX_LEN 32U
105#define PROV_MAX_APS 16U
217 if (nvs_open(
NS_PROV, NVS_READWRITE, &h) != ESP_OK) {
224 size_t n =
sizeof(
s_pass);
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; }
245 if (erased) { (void)nvs_commit(h); }
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]);
273 const int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
275 ESP_LOGE(
TAG,
"DNS socket failed");
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");
296 struct timeval tv = { 1, 0 };
297 (void)setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
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);
306 if (n < 17) {
continue; }
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;
316 if ((p == 0U) || ((p + 5U) >
static_cast<size_t>(n))) {
continue; }
317 const size_t q_end = p + 5U;
319 if ((q_end + 16U) >
sizeof(buf)) {
continue; }
323 buf[6] = 0x00U; buf[7] = 0x01U;
324 buf[8] = 0x00U; buf[9] = 0x00U;
325 buf[10] = 0x00U; buf[11] = 0x00U;
328 buf[a++] = 0xC0U; buf[a++] = 0x0CU;
329 buf[a++] = 0x00U; buf[a++] = 0x01U;
330 buf[a++] = 0x00U; buf[a++] = 0x01U;
331 buf[a++] = 0x00U; buf[a++] = 0x00U;
332 buf[a++] = 0x00U; buf[a++] = 0x00U;
333 buf[a++] = 0x00U; buf[a++] = 0x04U;
334 buf[a++] = 192U; buf[a++] = 168U; buf[a++] = 4U; buf[a++] = 1U;
336 (void)sendto(sock, buf, a, 0,
337 reinterpret_cast<struct sockaddr *
>(&from), from_len);
355static bool read_body(httpd_req_t *req,
char *out,
size_t n)
357 if (req->content_len >= n) {
return false; }
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);
386 if (
s_token[0] ==
'\0') {
return false; }
389 if (httpd_req_get_hdr_value_str(req,
"X-Prov-Token", tok,
390 sizeof(tok)) != ESP_OK) {
393 return CW_Utils::secure_compare(
reinterpret_cast<const uint8_t *
>(tok),
394 reinterpret_cast<const uint8_t *
>(
s_token),
405static esp_err_t
reply(httpd_req_t *req,
const char *status,
const char *msg)
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);
414static esp_err_t
ok(httpd_req_t *req,
const char *msg)
416 return reply(req,
"200 OK", msg);
421static bool uri_is(
const httpd_req_t *req,
const char *path)
423 const size_t n = strcspn(req->uri,
"?");
424 return (strlen(path) == n) && (strncmp(req->uri, path, n) == 0);
433static bool gate(httpd_req_t *req, esp_err_t *rc)
436 *rc =
reply(req,
"503 Service Unavailable",
437 "This page has closed. Reopen it on the terminal.");
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 "
454 *rc =
reply(req,
"401 Unauthorized",
455 "This browser is not authorized. Enter the admin code on "
456 "the terminal screen.");
472 httpd_resp_set_type(req,
"text/html");
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);
508 default:
return "idle";
533 (void)snprintf(body,
sizeof(body),
534 "{\"mode\":\"%s\",\"step\":\"%s\",\"authed\":false,"
535 "\"auth_pending\":%s,\"version\":\"%s\"}",
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);
588 char note[
sizeof(
s_note)];
590 (void)CW_Utils::safe_memcpy(
reinterpret_cast<uint8_t *
>(note),
sizeof(note),
591 reinterpret_cast<const uint8_t *
>(
s_note),
594 note[
sizeof(note) - 1U] =
'\0';
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}",
607 pay_eth, pay_trx, ct_eth, ct_trx,
608 ct_eth_own ?
"true" :
"false",
609 ct_trx_own ?
"true" :
"false",
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);
628 if (!
gate(req, &rc)) {
return rc; }
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++) {
641 if (!have) {
break; }
642 ap.
ssid[
sizeof(ap.
ssid) - 1U] =
'\0';
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);
653 (void)httpd_resp_sendstr_chunk(req,
"]}");
654 return httpd_resp_sendstr_chunk(req, NULL);
664 return reply(req,
"503 Service Unavailable",
665 "This page has closed. Reopen it on the terminal.");
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.");
685 s_token[i] =
"0123456789abcdef"[esp_random() & 0x0FU];
697 ESP_LOGW(
TAG,
"browser let in without a code (Wi-Fi-only re-join)");
703 ESP_LOGI(
TAG,
"browser asked to be authorised - admin code needed on panel");
730 if ((mbedtls_sha256(b, 21U, h, 0) != 0) ||
731 (mbedtls_sha256(h,
sizeof(h), h, 0) != 0)) {
734 return memcmp(h, &b[21], 4U) == 0;
744 if (!
gate(req, &rc)) {
return rc; }
746 char body[160] = { 0 };
747 if (!
read_body(req, body,
sizeof(body))) {
748 return reply(req,
"400 Bad Request",
"Bad request.");
753 (void)
form_field(body,
"addr", addr,
sizeof(addr));
754 (void)
form_field(body,
"net", net,
sizeof(net));
756 const bool tron = (strcmp(net,
"tron") == 0);
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.");
769 return reply(req,
"409 Conflict",
770 "Something is already waiting to be accepted on the "
771 "terminal screen. Deal with that one first.");
773 return ok(req,
"Now check that value on the terminal screen and accept it "
774 "there. It is not stored until you do.");
793 if (!
gate(req, &rc)) {
return rc; }
795 char body[96] = { 0 };
796 if (!
read_body(req, body,
sizeof(body))) {
797 return reply(req,
"400 Bad Request",
"Bad request.");
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));
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 "
819 return reply(req,
"400 Bad Request",
820 "Each fee has to be between 1 and 500 Gwei.");
823 return reply(req,
"400 Bad Request",
824 "The tip cannot be higher than the max fee.");
828 static_cast<uint32_t
>(prio_gwei));
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.");
856 if (!
gate(req, &rc)) {
return rc; }
858 char body[64] = { 0 };
859 if (!
read_body(req, body,
sizeof(body))) {
860 return reply(req,
"400 Bad Request",
"Bad request.");
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));
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.");
878 const long dst = strtol(dst_s, &dend, 10);
880 return reply(req,
"400 Bad Request",
881 "That is not a time zone the terminal can use.");
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.");
917 if (!
gate(req, &rc)) {
return rc; }
919 char body[48] = { 0 };
920 if (!
read_body(req, body,
sizeof(body))) {
921 return reply(req,
"400 Bad Request",
"Bad request.");
925 (void)
form_field(body,
"net", net,
sizeof(net));
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.");
932 return reply(req,
"409 Conflict",
"The terminal is already on that "
937 ESP_LOGW(
TAG,
"network switched to %s from the config page - restarting",
938 main_wanted ?
"PRODUCTION" :
"test");
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));
954 if (!
gate(req, &rc)) {
return rc; }
956 ESP_LOGI(
TAG,
"card-derived payout addresses requested from the page");
958 return ok(req,
"Follow the terminal screen.");
968 if (!
gate(req, &rc)) {
return rc; }
970 char body[256] = { 0 };
971 if (!
read_body(req, body,
sizeof(body))) {
972 return reply(req,
"400 Bad Request",
"Bad request.");
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));
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.");
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);
1001 const esp_err_t sent =
ok(req,
"Trying that network now. Watch the terminal "
1010 if (!
gate(req, &rc)) {
return rc; }
1012 return ok(req,
"Scanning.");
1018 if (!
gate(req, &rc)) {
return rc; }
1043 if (!
gate(req, &rc)) {
return rc; }
1045 const char *err =
"";
1046 if (!
ota_begin(req->content_len, &err)) {
1059 const size_t len = req->content_len;
1061 unsigned stalls = 0U;
1063 while (good && (got < len)) {
1065 const int n = httpd_req_recv(req,
reinterpret_cast<char *
>(
s_upload),
1067 if (n == HTTPD_SOCK_ERR_TIMEOUT) {
1071 ESP_LOGW(
TAG,
"upload stalled at %u/%u bytes - giving up",
1072 static_cast<unsigned>(got),
static_cast<unsigned>(len));
1080 ESP_LOGW(
TAG,
"upload aborted at %u/%u bytes",
1081 static_cast<unsigned>(got),
static_cast<unsigned>(len));
1086 got +=
static_cast<size_t>(n);
1091 return reply(req,
"400 Bad Request",
1092 "The upload did not complete. Nothing was installed.");
1096 const bool ended =
ota_end(ver,
sizeof(ver), &err);
1103 ESP_LOGI(
TAG,
"httpd stack: %u bytes still free after verification",
1104 static_cast<unsigned>(uxTaskGetStackHighWaterMark(NULL)));
1107 return reply(req,
"400 Bad Request", err);
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);
1119 const esp_err_t sent =
ok(req, msg);
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);
1169 httpd_resp_set_type(req,
"text/html");
1170 httpd_resp_set_hdr(req,
"Cache-Control",
"no-store");
1172 return httpd_resp_sendstr(req,
1173 "<HTML><HEAD><TITLE>Setup</TITLE></HEAD>"
1174 "<BODY><A href='" PORTAL_URL "'>Cryptnox setup</A></BODY></HTML>");
1183 const httpd_uri_t api[] = {
1185 {
"/api/state", HTTP_GET,
state_get, NULL },
1186 {
"/api/scan", HTTP_GET,
scan_get, NULL },
1187 {
"/api/auth", HTTP_POST,
auth_post, NULL },
1190 {
"/api/fees", HTTP_POST,
fees_post, NULL },
1191 {
"/api/clock", HTTP_POST,
clock_post, NULL },
1193 {
"/api/card", HTTP_POST,
card_post, NULL },
1194 {
"/api/wifi", HTTP_POST,
wifi_post, NULL },
1196 {
"/api/next", HTTP_POST,
next_post, NULL },
1197 {
"/api/ota", HTTP_POST,
ota_post, NULL },
1199 for (
size_t i = 0; i < (
sizeof(api) /
sizeof(api[0])); i++) {
1200 (void)httpd_register_uri_handler(
s_httpd, &api[i]);
1207 (void)httpd_register_uri_handler(
s_httpd, &p);
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);
1232 ESP_LOGE(
TAG,
"portal already up in mode %d - stop it first",
1233 static_cast<int>(
s_mode.load()));
1267 ESP_LOGE(
TAG,
"SoftAP failed to start");
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));
1275 httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
1276 cfg.max_uri_handlers = 24;
1302 cfg.stack_size = 16384;
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;
1324 cfg.server_port = 80;
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");
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));
1350 : (esp_timer_get_time() +
1357 ESP_LOGE(
TAG,
"DNS task failed - captive portal will NOT auto-open");
1361 ESP_LOGW(
TAG,
"%s portal up: SSID '%s', pass '%s', %s",
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));
1385 for (
int i = 0; (i < 20) && (
s_dns_task != NULL); i++) {
1386 vTaskDelay(pdMS_TO_TICKS(100));
1415 CW_Utils::secure_wipe(
reinterpret_cast<uint8_t *
>(
s_token),
sizeof(
s_token));
1421 CW_Utils::secure_wipe(
reinterpret_cast<uint8_t *
>(
s_pass),
sizeof(
s_pass));
1423 if (xSemaphoreTake(
s_ask_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
1425 CW_Utils::secure_wipe(
reinterpret_cast<uint8_t *
>(
s_ask_val),
1431 CW_Utils::secure_wipe(
reinterpret_cast<uint8_t *
>(
s_qr),
sizeof(
s_qr));
1433 ESP_LOGI(
TAG,
"config portal down");
1446 if (left <= 0) {
return 0U; }
1448 return static_cast<unsigned>((left + (59LL * 1000000LL)) / 60000000LL);
1463 ESP_LOGW(
TAG,
"browser authorised from the panel");
1470 CW_Utils::secure_wipe(
reinterpret_cast<uint8_t *
>(
s_token),
sizeof(
s_token));
1471 ESP_LOGW(
TAG,
"browser authorisation refused");
1479 if ((out == NULL) || (out_size < 5U) || (
s_token[0] ==
'\0')) {
return false; }
1481 for (
size_t i = 0U; i < 4U; i++) {
1483 v = (v << 4) | static_cast<unsigned>((c <=
'9') ? (c -
'0') : (c -
'a' + 10));
1485 (void)snprintf(out, out_size,
"%04u", v % 10000U);
1500 char clean[
sizeof(
s_note)];
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);
1511 (void)CW_Utils::safe_memcpy(
reinterpret_cast<uint8_t *
>(
s_note),
sizeof(
s_note),
1512 reinterpret_cast<const uint8_t *
>(clean), w + 1U);
1518 if (aps == NULL) { n = 0U; }
1521 for (uint16_t i = 0U; i < n; i++) {
s_aps[i] = aps[i]; }
1532 if (xSemaphoreTake(
s_ask_lock, pdMS_TO_TICKS(500)) != pdTRUE) {
return false; }
1541 ESP_LOGW(
TAG,
"%s proposed: %s - awaiting on-screen accept",
1548 char *value,
size_t value_n)
1551 if (xSemaphoreTake(
s_ask_lock, pdMS_TO_TICKS(100)) != pdTRUE) {
return false; }
1555 if (kind != NULL) { *kind =
s_ask; }
1556 if ((label != NULL) && (label_n > 0U)) {
1559 if ((value != NULL) && (value_n > 0U)) {
1560 (void)snprintf(value, value_n,
"%s",
s_ask_val);
1562 }
else if (kind != NULL) {
1572 if (xSemaphoreTake(
s_ask_lock, pdMS_TO_TICKS(100)) != pdTRUE) {
return false; }
1574 bool stored =
false;
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).
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.
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.
static const char *const TAG
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.
bool net_ap_start(const char *ssid, const char *pass)
Raise a WPA2 SoftAP as the radio's only interface, for phone-based configuration.
void net_ap_stop(void)
Drop the SoftAP, return the radio to station-only, and re-join.
void net_wifi_init(void)
Bring up the WiFi driver in station mode (idempotent).
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.
const char * ota_running_version(void)
The running firmware's version, from the image header.
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.
bool ota_receiving(void)
Whether an upload is in flight, so a second can be refused.
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.
bool ota_write(const void *buf, size_t n)
Append n bytes to the open slot.
bool ota_begin(size_t len, const char **err)
Open the idle slot for an image of len bytes.
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]
The config portal's document — the HTML/CSS half and the script half.
static const char *const PAGE_HTML
static const char *const PAGE_JS
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)
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[]
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)
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)
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.
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 bool read_body(httpd_req_t *req, char *out, size_t n)
Read a request body into out.
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 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)
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.
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.
prov_step_t
Where the wizard has got to.
#define PROV_WINDOW_MIN
How long the portal stays up before closing itself, minutes.
prov_mode_t
Which of the two portals is running.
bool settings_get_wifi(char *ssid, size_t ssid_n, char *pass, size_t pass_n)
Read the stored Wi-Fi credentials.
bool settings_set_tz_offset_min(int16_t minutes)
Store the panel clock's UTC offset.
bool settings_get_contract(pos_chain_t chain, char *out, size_t n)
Read the token-contract address for a network.
int16_t settings_get_tz_offset_min(void)
The panel clock's standard (winter) offset from UTC, in minutes east.
uint8_t settings_get_tz_dst(void)
The clock's DST rule, a civil_dst_t; CIVIL_DST_NONE when unset.
uint32_t settings_get_priority_fee_gwei(void)
EIP-1559 max priority fee (tip) per gas, in Gwei.
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.
bool settings_get_payout(bool tron, char *out, size_t n)
Read the payout address for a network.
void settings_set_mainnet(bool mainnet)
Persist the production/test network choice.
bool settings_set_contract(pos_chain_t chain, const char *addr)
Persist a token-contract address, value and echo copy.
bool settings_set_payout(bool tron, const char *addr)
Persist a payout address, writing both the value and its echo copy.
bool settings_set_tz_dst(uint8_t rule)
Store the DST rule.
bool settings_has_admin_code(void)
true once an admin code exists.
bool settings_get_mainnet(void)
true when the terminal is on the production networks.
uint32_t settings_get_max_fee_gwei(void)
EIP-1559 max fee per gas, in Gwei.
Persistent device settings stored in NVS (backlight, Wi-Fi creds).
#define SETTINGS_PAYOUT_MAX
Longest payout address plus NUL — "0x" + 40 hex, or 34 base58 Tron.
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.
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).
void ui_fees_changed(void)
Note that the stored gas caps have changed, so the Tx tab can catch up.
void ui_stage_wifi_creds(const char *ssid, const char *pass)
Load credentials into the same handoff buffers the picker fills.
void ui_clock_changed(void)
Note that the stored UTC offset has changed, so the clock can catch up.
@ UI_EVENT_PROV_VALUE_SET
void(* ui_event_cb_t)(ui_event_t event, uint64_t payload)
Callback invoked from the UI task on user interaction.