upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main/tollgate_api.c
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-15 22:27:14 +0530
committerYour Name <you@example.com>2026-05-15 22:27:14 +0530
commit1263d86314fc0760d9be8eea415ccecbc047a5eb (patch)
tree778130f0beb59d52f68e0e5f11388bf4b1470130 /main/tollgate_api.c
parenta7d0a672d59bf8985a6fc0e61b49015fabd96513 (diff)
Phase 2 WIP: Cashu payment endpoints, session tracking, updated checklist
- Add cashu.c/h: Cashu token decode (cashuA/base64url), proof state check via mint API, allotment calculator - Add session.c/h: time-based session management with allotment/expiry, spent secret tracking - Add tollgate_api.c/h: HTTP server on :2121 with GET / (kind=10021 discovery), POST / (payment processing), /usage, /whoami - Update captive portal HTML: replace Grant Free Access with Cashu token paste form + Pay & Connect button - Update tollgate_main.c: wire in session manager, TollGate API, 1s session tick loop - Add tests/phase2.mjs: Phase 2 test suite (discovery, invalid token, wrong mint, valid payment) - Update CHECKLIST.md: reflect Phase 1 complete, Phase 2 in progress with known bugs Known issues (not yet flashed): - Stack overflow crash in httpd POST handler (need stack_size=16384 + heap allocations) - cashu_decode_token uses 2KB stack buffer (needs heap alloc) - Mint URL should be testnut.cashu.space (nofee.testnut has API compat issues)
Diffstat (limited to 'main/tollgate_api.c')
-rw-r--r--main/tollgate_api.c355
1 files changed, 355 insertions, 0 deletions
diff --git a/main/tollgate_api.c b/main/tollgate_api.c
new file mode 100644
index 0000000..5ada3c7
--- /dev/null
+++ b/main/tollgate_api.c
@@ -0,0 +1,355 @@
1#include "tollgate_api.h"
2#include "cashu.h"
3#include "config.h"
4#include "session.h"
5#include "esp_log.h"
6#include "cJSON.h"
7#include "lwip/sockets.h"
8#include "lwip/netdb.h"
9#include <string.h>
10
11static const char *TAG = "tollgate_api";
12static httpd_handle_t s_api_server = NULL;
13
14static const char *TOLLGATE_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000";
15
16static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out)
17{
18 int sockfd = httpd_req_to_sockfd(req);
19 struct sockaddr_in addr;
20 socklen_t addr_len = sizeof(addr);
21 if (getpeername(sockfd, (struct sockaddr *)&addr, &addr_len) == 0) {
22 *ip_out = addr.sin_addr.s_addr;
23 return ESP_OK;
24 }
25 return ESP_FAIL;
26}
27
28static cJSON *create_notice(const char *level, const char *code, const char *content)
29{
30 cJSON *root = cJSON_CreateObject();
31 cJSON_AddNumberToObject(root, "kind", 21023);
32 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY);
33 cJSON *tags = cJSON_CreateArray();
34 cJSON *level_tag = cJSON_CreateArray();
35 cJSON_AddItemToArray(level_tag, cJSON_CreateString("level"));
36 cJSON_AddItemToArray(level_tag, cJSON_CreateString(level));
37 cJSON_AddItemToArray(tags, level_tag);
38 cJSON *code_tag = cJSON_CreateArray();
39 cJSON_AddItemToArray(code_tag, cJSON_CreateString("code"));
40 cJSON_AddItemToArray(code_tag, cJSON_CreateString(code));
41 cJSON_AddItemToArray(tags, code_tag);
42 cJSON_AddItemToObject(root, "tags", tags);
43 cJSON_AddStringToObject(root, "content", content);
44 return root;
45}
46
47static cJSON *create_session_event(uint32_t client_ip, uint64_t allotment_ms)
48{
49 cJSON *root = cJSON_CreateObject();
50 cJSON_AddNumberToObject(root, "kind", 1022);
51 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY);
52
53 cJSON *tags = cJSON_CreateArray();
54
55 cJSON *p_tag = cJSON_CreateArray();
56 cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
57 cJSON_AddItemToArray(p_tag, cJSON_CreateString("unknown"));
58 cJSON_AddItemToArray(tags, p_tag);
59
60 esp_ip4_addr_t ip = { .addr = client_ip };
61 char ip_str[16];
62 snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip));
63 cJSON *dev_tag = cJSON_CreateArray();
64 cJSON_AddItemToArray(dev_tag, cJSON_CreateString("device-identifier"));
65 cJSON_AddItemToArray(dev_tag, cJSON_CreateString("mac"));
66 cJSON_AddItemToArray(dev_tag, cJSON_CreateString(ip_str));
67 cJSON_AddItemToArray(tags, dev_tag);
68
69 cJSON *allotment_tag = cJSON_CreateArray();
70 cJSON_AddItemToArray(allotment_tag, cJSON_CreateString("allotment"));
71 char allotment_str[32];
72 snprintf(allotment_str, sizeof(allotment_str), "%llu", (unsigned long long)allotment_ms);
73 cJSON_AddItemToArray(allotment_tag, cJSON_CreateString(allotment_str));
74 cJSON_AddItemToArray(tags, allotment_tag);
75
76 cJSON *metric_tag = cJSON_CreateArray();
77 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric"));
78 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds"));
79 cJSON_AddItemToArray(tags, metric_tag);
80
81 cJSON_AddItemToObject(root, "tags", tags);
82 cJSON_AddStringToObject(root, "content", "");
83 return root;
84}
85
86static esp_err_t api_get_discovery(httpd_req_t *req)
87{
88 const tollgate_config_t *cfg = tollgate_config_get();
89
90 cJSON *root = cJSON_CreateObject();
91 cJSON_AddNumberToObject(root, "kind", 10021);
92 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY);
93
94 cJSON *tags = cJSON_CreateArray();
95
96 cJSON *metric_tag = cJSON_CreateArray();
97 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric"));
98 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds"));
99 cJSON_AddItemToArray(tags, metric_tag);
100
101 cJSON *step_tag = cJSON_CreateArray();
102 cJSON_AddItemToArray(step_tag, cJSON_CreateString("step_size"));
103 char step_str[32];
104 snprintf(step_str, sizeof(step_str), "%d", cfg->step_size_ms);
105 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str));
106 cJSON_AddItemToArray(tags, step_tag);
107
108 cJSON *price_tag = cJSON_CreateArray();
109 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
110 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
111 char price_str[32];
112 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step);
113 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
114 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
115 cJSON_AddItemToArray(price_tag, cJSON_CreateString(cfg->mint_url));
116 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
117 cJSON_AddItemToArray(tags, price_tag);
118
119 cJSON *tips_tag = cJSON_CreateArray();
120 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("tips"));
121 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("1"));
122 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("2"));
123 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5"));
124 cJSON_AddItemToArray(tags, tips_tag);
125
126 cJSON_AddItemToObject(root, "tags", tags);
127 cJSON_AddStringToObject(root, "content", "");
128
129 char *json = cJSON_PrintUnformatted(root);
130 httpd_resp_set_type(req, "application/json");
131 httpd_resp_send(req, json, strlen(json));
132 cJSON_free(json);
133 cJSON_Delete(root);
134 return ESP_OK;
135}
136
137static esp_err_t api_post_payment(httpd_req_t *req)
138{
139 uint32_t client_ip = 0;
140 get_client_ip(req, &client_ip);
141
142 int content_len = req->content_len;
143 if (content_len <= 0 || content_len > 16384) {
144 cJSON *notice = create_notice("error", "payment-error-invalid", "Invalid request body");
145 char *json = cJSON_PrintUnformatted(notice);
146 httpd_resp_set_status(req, "400 Bad Request");
147 httpd_resp_set_type(req, "application/json");
148 httpd_resp_send(req, json, strlen(json));
149 cJSON_free(json);
150 cJSON_Delete(notice);
151 return ESP_OK;
152 }
153
154 char *body = malloc(content_len + 1);
155 if (!body) {
156 cJSON *notice = create_notice("error", "session-error", "Out of memory");
157 char *json = cJSON_PrintUnformatted(notice);
158 httpd_resp_set_status(req, "503 Service Unavailable");
159 httpd_resp_set_type(req, "application/json");
160 httpd_resp_send(req, json, strlen(json));
161 cJSON_free(json);
162 cJSON_Delete(notice);
163 return ESP_OK;
164 }
165 int received = httpd_req_recv(req, body, content_len);
166 if (received <= 0) {
167 free(body);
168 httpd_resp_set_status(req, "400 Bad Request");
169 httpd_resp_set_type(req, "text/plain");
170 httpd_resp_send(req, "bad request", 11);
171 return ESP_OK;
172 }
173 body[received] = '\0';
174
175 ESP_LOGI(TAG, "Payment received: %d bytes", received);
176
177 cashu_token_t token;
178 esp_err_t err = cashu_decode_token(body, &token);
179 free(body);
180
181 if (err != ESP_OK) {
182 cJSON *notice = create_notice("error", "payment-error-invalid", "Failed to decode Cashu token");
183 char *json = cJSON_PrintUnformatted(notice);
184 httpd_resp_set_status(req, "400 Bad Request");
185 httpd_resp_set_type(req, "application/json");
186 httpd_resp_send(req, json, strlen(json));
187 cJSON_free(json);
188 cJSON_Delete(notice);
189 return ESP_OK;
190 }
191
192 const char *mint_url = token.mint_url[0] ? token.mint_url : tollgate_config_get()->mint_url;
193 if (!cashu_is_mint_accepted(mint_url)) {
194 cJSON *notice = create_notice("error", "payment-error-mint-not-accepted", "Mint not accepted");
195 char *json = cJSON_PrintUnformatted(notice);
196 httpd_resp_set_status(req, "402 Payment Required");
197 httpd_resp_set_type(req, "application/json");
198 httpd_resp_send(req, json, strlen(json));
199 cJSON_free(json);
200 cJSON_Delete(notice);
201 return ESP_OK;
202 }
203
204 for (int i = 0; i < token.proof_count; i++) {
205 if (session_is_secret_spent(token.proofs[i].secret)) {
206 cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent");
207 char *json = cJSON_PrintUnformatted(notice);
208 httpd_resp_set_status(req, "402 Payment Required");
209 httpd_resp_set_type(req, "application/json");
210 httpd_resp_send(req, json, strlen(json));
211 cJSON_free(json);
212 cJSON_Delete(notice);
213 return ESP_OK;
214 }
215 }
216
217 cashu_proof_state_t states[CASHU_MAX_PROOFS];
218 int state_count = 0;
219 err = cashu_check_proof_states(mint_url, &token, states, &state_count);
220 if (err != ESP_OK) {
221 cJSON *notice = create_notice("error", "payment-error-verification", "Failed to verify token with mint");
222 char *json = cJSON_PrintUnformatted(notice);
223 httpd_resp_set_status(req, "502 Bad Gateway");
224 httpd_resp_set_type(req, "application/json");
225 httpd_resp_send(req, json, strlen(json));
226 cJSON_free(json);
227 cJSON_Delete(notice);
228 return ESP_OK;
229 }
230
231 for (int i = 0; i < state_count; i++) {
232 if (states[i].spent) {
233 cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent");
234 char *json = cJSON_PrintUnformatted(notice);
235 httpd_resp_set_status(req, "402 Payment Required");
236 httpd_resp_set_type(req, "application/json");
237 httpd_resp_send(req, json, strlen(json));
238 cJSON_free(json);
239 cJSON_Delete(notice);
240 return ESP_OK;
241 }
242 }
243
244 const tollgate_config_t *cfg = tollgate_config_get();
245 uint64_t allotment = cashu_calculate_allotment_ms(token.total_amount, cfg->price_per_step, cfg->step_size_ms);
246 if (allotment == 0) {
247 cJSON *notice = create_notice("error", "payment-error-insufficient", "Token value too low");
248 char *json = cJSON_PrintUnformatted(notice);
249 httpd_resp_set_status(req, "402 Payment Required");
250 httpd_resp_set_type(req, "application/json");
251 httpd_resp_send(req, json, strlen(json));
252 cJSON_free(json);
253 cJSON_Delete(notice);
254 return ESP_OK;
255 }
256
257 const char *secrets[5];
258 for (int i = 0; i < token.proof_count && i < 5; i++) {
259 secrets[i] = token.proofs[i].secret;
260 }
261 session_t *session = session_create(client_ip, allotment, secrets, token.proof_count);
262 if (!session) {
263 cJSON *notice = create_notice("error", "session-error", "Failed to create session");
264 char *json = cJSON_PrintUnformatted(notice);
265 httpd_resp_set_status(req, "503 Service Unavailable");
266 httpd_resp_set_type(req, "application/json");
267 httpd_resp_send(req, json, strlen(json));
268 cJSON_free(json);
269 cJSON_Delete(notice);
270 return ESP_OK;
271 }
272
273 cJSON *session_event = create_session_event(client_ip, allotment);
274 char *json = cJSON_PrintUnformatted(session_event);
275 httpd_resp_set_type(req, "application/json");
276 httpd_resp_send(req, json, strlen(json));
277 cJSON_free(json);
278 cJSON_Delete(session_event);
279 return ESP_OK;
280}
281
282static esp_err_t api_get_usage(httpd_req_t *req)
283{
284 uint32_t client_ip = 0;
285 get_client_ip(req, &client_ip);
286
287 session_t *session = session_find_by_ip(client_ip);
288 if (!session || !session->active) {
289 httpd_resp_set_type(req, "text/plain");
290 httpd_resp_send(req, "-1/-1", 5);
291 return ESP_OK;
292 }
293
294 int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms;
295 int64_t remaining = session->allotment_ms - elapsed;
296 if (remaining < 0) remaining = 0;
297
298 char resp[64];
299 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms);
300 httpd_resp_set_type(req, "text/plain");
301 httpd_resp_send(req, resp, strlen(resp));
302 return ESP_OK;
303}
304
305static esp_err_t api_get_whoami(httpd_req_t *req)
306{
307 uint32_t client_ip = 0;
308 char resp[64];
309 if (get_client_ip(req, &client_ip) == ESP_OK) {
310 esp_ip4_addr_t ip = { .addr = client_ip };
311 snprintf(resp, sizeof(resp), "mac=" IPSTR, IP2STR(&ip));
312 } else {
313 snprintf(resp, sizeof(resp), "mac=unknown");
314 }
315 httpd_resp_set_type(req, "text/plain");
316 httpd_resp_send(req, resp, strlen(resp));
317 return ESP_OK;
318}
319
320static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery };
321static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment };
322static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage };
323static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = api_get_whoami };
324
325esp_err_t tollgate_api_start(void)
326{
327 if (s_api_server) return ESP_OK;
328
329 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
330 config.server_port = 2121;
331 config.ctrl_port = 32769;
332 config.max_uri_handlers = 10;
333
334 esp_err_t ret = httpd_start(&s_api_server, &config);
335 if (ret != ESP_OK) {
336 ESP_LOGE(TAG, "Failed to start API server: %s", esp_err_to_name(ret));
337 return ret;
338 }
339
340 httpd_register_uri_handler(s_api_server, &uri_discovery);
341 httpd_register_uri_handler(s_api_server, &uri_payment);
342 httpd_register_uri_handler(s_api_server, &uri_usage);
343 httpd_register_uri_handler(s_api_server, &uri_whoami);
344
345 ESP_LOGI(TAG, "TollGate API started on port 2121");
346 return ESP_OK;
347}
348
349void tollgate_api_stop(void)
350{
351 if (s_api_server) {
352 httpd_stop(s_api_server);
353 s_api_server = NULL;
354 }
355}