From c342801162e62ff017ead18688107397d229f606 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 16 May 2026 03:37:20 +0530 Subject: Phase 2 WIP: token decode works, TLS checkstate succeeds (crashes after response) - cashu.c: dynamic json_buf sizing (was 2048 stack, now heap based on token length) - cashu.c: strip trailing newline/CR from token input (cashu CLI appends 'Balance: 0 sat') - cashu.c: esp_crt_bundle_attach for HTTPS to mint API - cashu.c: esp_http_client_open/write/fetch_headers/read pattern for HTTPS POST - cashu.c: remove debug b64url_decode logging - tollgate_api.c: loop httpd_req_recv for full body (was single call, missed TCP segments) - tollgate_api.c: stack_size=32768 for TLS in httpd handler - config.c: fix default mint URL from nofee.testnut to testnut.cashu.space - CMakeLists.txt: add esp-tls dependency for cert bundle - CHECKLIST.md: updated with infrastructure status and TDD plan Known issue: device reboots after checkstate returns 966 bytes with status=200. Crash likely in post-response processing (JSON parse or session create). --- CHECKLIST.md | 89 +++++++++++++++++++++++++++-------------------------- main/CMakeLists.txt | 2 +- main/cashu.c | 32 +++++++++++++------ main/config.c | 2 +- main/tollgate_api.c | 25 +++++++++------ 5 files changed, 84 insertions(+), 66 deletions(-) diff --git a/CHECKLIST.md b/CHECKLIST.md index fe5894f..ba17adc 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,6 +1,6 @@ # TollGate ESP32 — Progress Checklist -## Phase 0: Bootstrap +## Phase 0: Bootstrap — COMPLETE - [x] Create project directory and git repo - [x] Create .env, .env.example, .gitignore - [x] Persist PLAN.md and CHECKLIST.md @@ -9,66 +9,67 @@ - [x] Run `make detect-all` — identified both boards as ESP32-S3 (16MB flash) - [x] Fix ESP-IDF v5.4.1 installation (was deeply corrupted, re-cloned) -## Phase 1: Captive Portal + Firewall — COMPLETE +## Phase 1: Captive Portal + Firewall — COMPLETE (commit `a7d0a67`) - [x] Implement tollgate_main.c (WiFi AP+STA, event loop) - [x] Implement config.c/h (SPIFFS JSON config loading) - [x] Implement dns_server.c/h (DNS hijack/forward per-client) - [x] Implement captive_portal.c/h (HTTP :80, portal HTML) - [x] Implement firewall.c/h (NAPT on/off per auth state) - [x] Set up test infrastructure (Node.js tests, helpers, Playwright) -- [x] Fix WiFi init order bug (netif before esp_wifi_init, set_mode before set_config) -- [x] Fix DNS hijack test (nslookup exits 1 for AAAA, catch stderr) -- [x] Fix ping tests (use `-I wlp59s0` to force through TollGate AP) -- [x] Test 1: Boot and AP appears -- [x] Test 2: DHCP lease -- [x] Test 3: Captive portal serves HTML -- [x] Test 4: Captive detection URIs work (8 URIs) -- [x] Test 5: DNS hijack before auth -- [x] Test 6: No internet before auth -- [x] Test 7: /whoami returns MAC -- [x] Test 8: /usage returns no session -- [x] Test 9: Grant access via API -- [x] Test 10: DNS forward after auth -- [x] Test 11: Internet after auth -- [x] Test 12: HTTP browsing works -- [x] Test 13: Reset auth -- [x] Test 14: Internet blocked after reset -- [x] **All 20 API tests pass, all 6 smoke tests pass** -- [x] Committed: `a7d0a67` +- [x] Fix WiFi init order bug +- [x] Fix DNS hijack test (nslookup exits 1 for AAAA) +- [x] Fix ping tests (use `-I wlp59s0`) +- [x] Tests 1-14: ALL PASSING -## Phase 2: E-Cash Payments — IN PROGRESS (code written, bugs to fix) +## Phase 2: E-Cash Payments — IN PROGRESS +### Code Written (commit `1263d86`) - [x] Implement cashu.c/h (Cashu token parse, base64url, checkstate, mint validation) - [x] Implement session.c/h (time-based allotment, expiry, secret tracking) - [x] Implement tollgate_api.c/h (:2121 server, GET/POST /, /usage, /whoami) -- [x] Update captive portal HTML with payment form (token textarea, Pay & Connect button) +- [x] Update captive portal HTML with payment form - [x] Wire into tollgate_main.c (session_init, api_start, session_tick loop) + +### Bug Fixes (commit `aed51d8`) +- [x] Stack overflow: httpd stack_size increased to 16384 in tollgate_api.c +- [x] Heap allocations: b64, json_buf, post_body, resp_buf moved to heap in cashu.c +- [x] .env: MINT_URL updated to testnut.cashu.space +- [x] Makefile: replaced Go-based tokens target with nutshell wallet targets + +### Infrastructure (ready now) +- [x] Upstream gateway on enx00e04c633a90 (192.168.2.0/24, metric 101, default route) +- [x] OpenWRT TollGate on enx00e04c683d2d (10.47.41.0/24, metric 20100, never-default) +- [x] WiFi wlp59s0 free for ESP32 TollGate connection +- [x] NetworkManager profile "TollGate-ESP32" created (manual 192.168.4.2/24, autoconnect=no) + +### Tests Passing - [x] Test 15: Advertisement valid (kind=10021 with price_per_step) — PASSING -- [ ] **BUG FIX: Stack overflow in httpd task** — POST to :2121 crashes (Guru Meditation LoadProhibited). Need to increase httpd stack_size to 16384 and heap-allocate large buffers in cashu.c -- [ ] **BUG FIX: cashu_decode_token has 2048B stack buffer** — move json_buf to heap -- [ ] **BUG FIX: cashu_check_proof_states has 4096B stack buffer** — move resp_buf to heap -- [ ] Test 16: Valid payment (needs valid Cashu token from nutshell) -- [ ] Test 17: Usage tracking after payment -- [ ] Test 18: Internet after payment -- [ ] Test 19: Invalid token rejected — blocked by stack overflow crash -- [ ] Test 20: Spent token rejected -- [ ] Test 21: Wrong mint rejected — blocked by stack overflow crash -- [ ] Test 22: Session expiry -- [ ] Test 23: Session renewal -- [ ] Test 24: Portal payment form — blocked by stack overflow crash + +### Tests Blocked (need hardware flash + test) +- [ ] Test 16: Valid payment (POST :2121/ with valid Cashu token → kind=1022 session) +- [ ] Test 17: Usage tracking after payment (GET :2121/usage → active usage) +- [ ] Test 18: Internet after payment (ping through TollGate works) +- [ ] Test 19: Invalid token rejected (POST garbage → 400, kind=21023) +- [ ] Test 20: Spent token rejected (reuse token → 402, kind=21023) +- [ ] Test 21: Wrong mint rejected (POST token from wrong mint → 402) +- [ ] Test 22: Session expiry (wait for allotment → internet blocked) +- [ ] Test 23: Session renewal (second payment → allotment extended) +- [ ] Test 24: Portal payment form visible in browser - [ ] Test 25: Two clients pay independently -- [ ] Test 26: Client isolation -- [ ] Test 27: Full e2e browser flow +- [ ] Test 26: Client isolation (only payer gets internet) +- [ ] Test 27: Full e2e: portal → pay → browse -## Infrastructure Setup — TODO (before next hardware session) -- [ ] Update .env: change mint from nofee.testnut.cashu.space → testnut.cashu.space -- [ ] Update Makefile: add nutshell wallet targets (mint-token, send-token, balance) -- [ ] Create Ansible playbook for full dev environment setup -- [ ] Create NetworkManager profile for TollGate testing (ethernet=upstream, wifi=tollgate only) -- [ ] Verify network routing works (ethernet default route, WiFi 192.168.4.0/24 only) +### Next Steps (TDD cycle) +1. Flash firmware to ESP32 board A (`make flash-a`) +2. Connect WiFi to TollGate AP: `nmcli con up TollGate-ESP32` +3. Run Phase 2 discovery test: `TOLLGATE_IP=192.168.4.1 node tests/phase2.mjs` +4. If Test 15 still passes, proceed to Test 19 (invalid token — no mint needed) +5. Mint a test token: `make mint-token AMOUNT=21` +6. Run full Phase 2 with token: `TEST_TOKEN=$(cashu --env-mint testnut.cashu.space send --legacy 21) TOLLGATE_IP=192.168.4.1 node tests/phase2.mjs` +7. Fix any failures, commit + push when tests pass ## Phase 3: nucula Wallet + Reseller — NOT STARTED - [ ] Extract nucula wallet into components/cashu_wallet/ - [ ] Replace simple melt with Wallet::receive() - [ ] Implement payout.c/h (background melt-to-LN) - [ ] Implement upstream_client.c/h (reseller mode) -- [ ] Test 28-38: All Phase 3 tests +- [ ] Tests 28-38 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 97b4c37..5650309 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -8,5 +8,5 @@ idf_component_register(SRCS "tollgate_main.c" "tollgate_api.c" INCLUDE_DIRS "." "${IDF_PATH}/components/lwip/include/apps" REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server - lwip json esp_http_client mbedtls log spiffs + lwip json esp_http_client mbedtls esp-tls log spiffs PRIV_REQUIRES esp-tls) diff --git a/main/cashu.c b/main/cashu.c index 8dffacc..ba6d9ef 100644 --- a/main/cashu.c +++ b/main/cashu.c @@ -5,7 +5,7 @@ #include "cJSON.h" #include "mbedtls/base64.h" #include "mbedtls/sha256.h" -#include +#include "esp_crt_bundle.h" static const char *TAG = "cashu"; @@ -78,6 +78,10 @@ esp_err_t cashu_decode_token(const char *token_str, cashu_token_t *out) memset(out, 0, sizeof(*out)); size_t len = strlen(token_str); + char *nl = strchr(token_str, '\n'); + if (nl) len = nl - token_str; + char *cr = strchr(token_str, '\r'); + if (cr && (cr - token_str) < (int)len) len = cr - token_str; if (len <= V3_PREFIX_LEN) { ESP_LOGE(TAG, "Token too short"); return ESP_FAIL; @@ -87,11 +91,13 @@ esp_err_t cashu_decode_token(const char *token_str, cashu_token_t *out) return ESP_FAIL; } - char *json_buf = malloc(2048); + size_t b64_len = len - V3_PREFIX_LEN; + size_t decoded_size = (b64_len * 3) / 4 + 4; + char *json_buf = malloc(decoded_size); if (!json_buf) return ESP_FAIL; size_t json_len = 0; - if (b64url_decode(token_str + V3_PREFIX_LEN, len - V3_PREFIX_LEN, - json_buf, 2047, &json_len) != 0) { + if (b64url_decode(token_str + V3_PREFIX_LEN, b64_len, + json_buf, decoded_size - 1, &json_len) != 0) { ESP_LOGE(TAG, "Base64url decode failed"); free(json_buf); return ESP_FAIL; @@ -181,12 +187,12 @@ esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *to char *resp_buf = malloc(8192); if (!resp_buf) { free(post_body); return ESP_FAIL; } - int resp_len = 0; esp_http_client_config_t config = { .url = url, .method = HTTP_METHOD_POST, - .timeout_ms = 10000, + .timeout_ms = 15000, + .crt_bundle_attach = esp_crt_bundle_attach, }; esp_http_client_handle_t client = esp_http_client_init(&config); if (!client) { free(post_body); free(resp_buf); return ESP_FAIL; } @@ -194,20 +200,26 @@ esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *to esp_http_client_set_header(client, "Content-Type", "application/json"); esp_err_t err = esp_http_client_open(client, strlen(post_body)); if (err != ESP_OK) { + ESP_LOGE(TAG, "checkstate open failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); free(post_body); free(resp_buf); - return err; + return ESP_FAIL; } - esp_http_client_write(client, post_body, strlen(post_body)); + int written = esp_http_client_write(client, post_body, strlen(post_body)); free(post_body); + ESP_LOGI(TAG, "checkstate written %d bytes", written); - resp_len = esp_http_client_read(client, resp_buf, 8191); + int content_length = esp_http_client_fetch_headers(client); int status = esp_http_client_get_status_code(client); + ESP_LOGI(TAG, "checkstate headers: status=%d, content_length=%d", status, content_length); + + int resp_len = esp_http_client_read(client, resp_buf, 8191); + ESP_LOGI(TAG, "checkstate read: resp_len=%d", resp_len); esp_http_client_cleanup(client); if (status != 200 || resp_len <= 0) { - ESP_LOGE(TAG, "checkstate returned %d", status); + ESP_LOGE(TAG, "checkstate failed: status=%d, resp_len=%d", status, resp_len); free(resp_buf); return ESP_FAIL; } diff --git a/main/config.c b/main/config.c index f78bc8b..b44c3c5 100644 --- a/main/config.c +++ b/main/config.c @@ -38,7 +38,7 @@ esp_err_t tollgate_config_init(void) "\"ap_ssid\":\"TollGate\"," "\"ap_password\":\"\"," "\"ap_channel\":1," - "\"mint_url\":\"https://nofee.testnut.cashu.space\"," + "\"mint_url\":\"https://testnut.cashu.space\"," "\"lnurl_url\":\"https://redeem.cashu.me/.well-known/lnurlp/tollgate\"," "\"price_per_step\":21," "\"step_size_ms\":60000" diff --git a/main/tollgate_api.c b/main/tollgate_api.c index b2ad647..2af04bc 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c @@ -162,17 +162,22 @@ static esp_err_t api_post_payment(httpd_req_t *req) cJSON_Delete(notice); return ESP_OK; } - int received = httpd_req_recv(req, body, content_len); - if (received <= 0) { - free(body); - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_set_type(req, "text/plain"); - httpd_resp_send(req, "bad request", 11); - return ESP_OK; + int received = 0; + int total = 0; + while (total < content_len) { + received = httpd_req_recv(req, body + total, content_len - total); + if (received <= 0) { + free(body); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "text/plain"); + httpd_resp_send(req, "bad request", 11); + return ESP_OK; + } + total += received; } - body[received] = '\0'; + body[total] = '\0'; - ESP_LOGI(TAG, "Payment received: %d bytes", received); + ESP_LOGI(TAG, "Payment received: %d bytes", total); cashu_token_t token; esp_err_t err = cashu_decode_token(body, &token); @@ -330,7 +335,7 @@ esp_err_t tollgate_api_start(void) config.server_port = 2121; config.ctrl_port = 32769; config.max_uri_handlers = 10; - config.stack_size = 16384; + config.stack_size = 32768; esp_err_t ret = httpd_start(&s_api_server, &config); if (ret != ESP_OK) { -- cgit v1.2.3