diff options
| author | Your Name <you@example.com> | 2026-05-16 04:46:32 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-16 04:46:32 +0530 |
| commit | 50b5975ac8793d6d820c35b5999f8a909f64e71b (patch) | |
| tree | 2592f9e7a671af2aca56e46887e50b8ad8e418b6 /main/tollgate_api.c | |
| parent | 3f46bb83cb1041889034c88adce1895dd330793f (diff) | |
Captive portal detection fix + Phase 2 tests 16-18,20 passing (17/17)
- Add DoT reject server on port 853 (TCP RST forces DNS-over-TLS fallback)
- DNS hijack returns NXDOMAIN for all non-A query types (no forwarding for unauthed)
- Shorter TTL on hijack responses (10s) for faster captive detection
- Explicit 302 redirect handlers for /generate_204, /hotspot-detect.html, etc.
- HTTP and DNS request logging for debugging captive detection
- Per-MAC tracking in firewall (find_by_mac, get_mac_for_ip with ARP fallback)
- Session MAC tracking (session_find_by_mac)
- Phase 2 test 18: add route through TollGate before ping test
- All 17 Phase 2 tests pass (15-21 + whoami + portal form)
Diffstat (limited to 'main/tollgate_api.c')
| -rw-r--r-- | main/tollgate_api.c | 69 |
1 files changed, 55 insertions, 14 deletions
diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 2af04bc..efb5cdf 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c | |||
| @@ -2,10 +2,12 @@ | |||
| 2 | #include "cashu.h" | 2 | #include "cashu.h" |
| 3 | #include "config.h" | 3 | #include "config.h" |
| 4 | #include "session.h" | 4 | #include "session.h" |
| 5 | #include "firewall.h" | ||
| 5 | #include "esp_log.h" | 6 | #include "esp_log.h" |
| 6 | #include "cJSON.h" | 7 | #include "cJSON.h" |
| 7 | #include "lwip/sockets.h" | 8 | #include "lwip/sockets.h" |
| 8 | #include "lwip/netdb.h" | 9 | #include "lwip/netdb.h" |
| 10 | #include "freertos/task.h" | ||
| 9 | #include <string.h> | 11 | #include <string.h> |
| 10 | 12 | ||
| 11 | static const char *TAG = "tollgate_api"; | 13 | static const char *TAG = "tollgate_api"; |
| @@ -179,11 +181,22 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 179 | 181 | ||
| 180 | ESP_LOGI(TAG, "Payment received: %d bytes", total); | 182 | ESP_LOGI(TAG, "Payment received: %d bytes", total); |
| 181 | 183 | ||
| 182 | cashu_token_t token; | 184 | cashu_token_t *token = malloc(sizeof(cashu_token_t)); |
| 183 | esp_err_t err = cashu_decode_token(body, &token); | 185 | if (!token) { |
| 186 | cJSON *notice = create_notice("error", "session-error", "Out of memory"); | ||
| 187 | char *json = cJSON_PrintUnformatted(notice); | ||
| 188 | httpd_resp_set_status(req, "503 Service Unavailable"); | ||
| 189 | httpd_resp_set_type(req, "application/json"); | ||
| 190 | httpd_resp_send(req, json, strlen(json)); | ||
| 191 | cJSON_free(json); | ||
| 192 | cJSON_Delete(notice); | ||
| 193 | return ESP_OK; | ||
| 194 | } | ||
| 195 | esp_err_t err = cashu_decode_token(body, token); | ||
| 184 | free(body); | 196 | free(body); |
| 185 | 197 | ||
| 186 | if (err != ESP_OK) { | 198 | if (err != ESP_OK) { |
| 199 | free(token); | ||
| 187 | cJSON *notice = create_notice("error", "payment-error-invalid", "Failed to decode Cashu token"); | 200 | cJSON *notice = create_notice("error", "payment-error-invalid", "Failed to decode Cashu token"); |
| 188 | char *json = cJSON_PrintUnformatted(notice); | 201 | char *json = cJSON_PrintUnformatted(notice); |
| 189 | httpd_resp_set_status(req, "400 Bad Request"); | 202 | httpd_resp_set_status(req, "400 Bad Request"); |
| @@ -194,8 +207,9 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 194 | return ESP_OK; | 207 | return ESP_OK; |
| 195 | } | 208 | } |
| 196 | 209 | ||
| 197 | const char *mint_url = token.mint_url[0] ? token.mint_url : tollgate_config_get()->mint_url; | 210 | const char *mint_url = token->mint_url[0] ? token->mint_url : tollgate_config_get()->mint_url; |
| 198 | if (!cashu_is_mint_accepted(mint_url)) { | 211 | if (!cashu_is_mint_accepted(mint_url)) { |
| 212 | free(token); | ||
| 199 | cJSON *notice = create_notice("error", "payment-error-mint-not-accepted", "Mint not accepted"); | 213 | cJSON *notice = create_notice("error", "payment-error-mint-not-accepted", "Mint not accepted"); |
| 200 | char *json = cJSON_PrintUnformatted(notice); | 214 | char *json = cJSON_PrintUnformatted(notice); |
| 201 | httpd_resp_set_status(req, "402 Payment Required"); | 215 | httpd_resp_set_status(req, "402 Payment Required"); |
| @@ -206,8 +220,9 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 206 | return ESP_OK; | 220 | return ESP_OK; |
| 207 | } | 221 | } |
| 208 | 222 | ||
| 209 | for (int i = 0; i < token.proof_count; i++) { | 223 | for (int i = 0; i < token->proof_count; i++) { |
| 210 | if (session_is_secret_spent(token.proofs[i].secret)) { | 224 | if (session_is_secret_spent(token->proofs[i].secret)) { |
| 225 | free(token); | ||
| 211 | cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); | 226 | cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); |
| 212 | char *json = cJSON_PrintUnformatted(notice); | 227 | char *json = cJSON_PrintUnformatted(notice); |
| 213 | httpd_resp_set_status(req, "402 Payment Required"); | 228 | httpd_resp_set_status(req, "402 Payment Required"); |
| @@ -219,10 +234,24 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 219 | } | 234 | } |
| 220 | } | 235 | } |
| 221 | 236 | ||
| 222 | cashu_proof_state_t states[CASHU_MAX_PROOFS]; | 237 | cashu_proof_state_t *states = malloc(CASHU_MAX_PROOFS * sizeof(cashu_proof_state_t)); |
| 238 | if (!states) { | ||
| 239 | free(token); | ||
| 240 | cJSON *notice = create_notice("error", "session-error", "Out of memory"); | ||
| 241 | char *json = cJSON_PrintUnformatted(notice); | ||
| 242 | httpd_resp_set_status(req, "503 Service Unavailable"); | ||
| 243 | httpd_resp_set_type(req, "application/json"); | ||
| 244 | httpd_resp_send(req, json, strlen(json)); | ||
| 245 | cJSON_free(json); | ||
| 246 | cJSON_Delete(notice); | ||
| 247 | return ESP_OK; | ||
| 248 | } | ||
| 223 | int state_count = 0; | 249 | int state_count = 0; |
| 224 | err = cashu_check_proof_states(mint_url, &token, states, &state_count); | 250 | err = cashu_check_proof_states(mint_url, token, states, &state_count); |
| 251 | ESP_LOGI(TAG, "Stack HWM after checkstate: %u", uxTaskGetStackHighWaterMark(NULL)); | ||
| 225 | if (err != ESP_OK) { | 252 | if (err != ESP_OK) { |
| 253 | free(states); | ||
| 254 | free(token); | ||
| 226 | cJSON *notice = create_notice("error", "payment-error-verification", "Failed to verify token with mint"); | 255 | cJSON *notice = create_notice("error", "payment-error-verification", "Failed to verify token with mint"); |
| 227 | char *json = cJSON_PrintUnformatted(notice); | 256 | char *json = cJSON_PrintUnformatted(notice); |
| 228 | httpd_resp_set_status(req, "502 Bad Gateway"); | 257 | httpd_resp_set_status(req, "502 Bad Gateway"); |
| @@ -235,6 +264,8 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 235 | 264 | ||
| 236 | for (int i = 0; i < state_count; i++) { | 265 | for (int i = 0; i < state_count; i++) { |
| 237 | if (states[i].spent) { | 266 | if (states[i].spent) { |
| 267 | free(states); | ||
| 268 | free(token); | ||
| 238 | cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); | 269 | cJSON *notice = create_notice("error", "payment-error-token-spent", "Token already spent"); |
| 239 | char *json = cJSON_PrintUnformatted(notice); | 270 | char *json = cJSON_PrintUnformatted(notice); |
| 240 | httpd_resp_set_status(req, "402 Payment Required"); | 271 | httpd_resp_set_status(req, "402 Payment Required"); |
| @@ -247,8 +278,10 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 247 | } | 278 | } |
| 248 | 279 | ||
| 249 | const tollgate_config_t *cfg = tollgate_config_get(); | 280 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 250 | uint64_t allotment = cashu_calculate_allotment_ms(token.total_amount, cfg->price_per_step, cfg->step_size_ms); | 281 | uint64_t allotment = cashu_calculate_allotment_ms(token->total_amount, cfg->price_per_step, cfg->step_size_ms); |
| 251 | if (allotment == 0) { | 282 | if (allotment == 0) { |
| 283 | free(states); | ||
| 284 | free(token); | ||
| 252 | cJSON *notice = create_notice("error", "payment-error-insufficient", "Token value too low"); | 285 | cJSON *notice = create_notice("error", "payment-error-insufficient", "Token value too low"); |
| 253 | char *json = cJSON_PrintUnformatted(notice); | 286 | char *json = cJSON_PrintUnformatted(notice); |
| 254 | httpd_resp_set_status(req, "402 Payment Required"); | 287 | httpd_resp_set_status(req, "402 Payment Required"); |
| @@ -259,11 +292,14 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 259 | return ESP_OK; | 292 | return ESP_OK; |
| 260 | } | 293 | } |
| 261 | 294 | ||
| 295 | int secret_count = token->proof_count > 5 ? 5 : token->proof_count; | ||
| 262 | const char *secrets[5]; | 296 | const char *secrets[5]; |
| 263 | for (int i = 0; i < token.proof_count && i < 5; i++) { | 297 | for (int i = 0; i < secret_count; i++) { |
| 264 | secrets[i] = token.proofs[i].secret; | 298 | secrets[i] = token->proofs[i].secret; |
| 265 | } | 299 | } |
| 266 | session_t *session = session_create(client_ip, allotment, secrets, token.proof_count); | 300 | session_t *session = session_create(client_ip, allotment, secrets, secret_count); |
| 301 | free(states); | ||
| 302 | free(token); | ||
| 267 | if (!session) { | 303 | if (!session) { |
| 268 | cJSON *notice = create_notice("error", "session-error", "Failed to create session"); | 304 | cJSON *notice = create_notice("error", "session-error", "Failed to create session"); |
| 269 | char *json = cJSON_PrintUnformatted(notice); | 305 | char *json = cJSON_PrintUnformatted(notice); |
| @@ -310,12 +346,17 @@ static esp_err_t api_get_usage(httpd_req_t *req) | |||
| 310 | static esp_err_t api_get_whoami(httpd_req_t *req) | 346 | static esp_err_t api_get_whoami(httpd_req_t *req) |
| 311 | { | 347 | { |
| 312 | uint32_t client_ip = 0; | 348 | uint32_t client_ip = 0; |
| 313 | char resp[64]; | 349 | char resp[96]; |
| 314 | if (get_client_ip(req, &client_ip) == ESP_OK) { | 350 | if (get_client_ip(req, &client_ip) == ESP_OK) { |
| 351 | char mac[18] = {0}; | ||
| 315 | esp_ip4_addr_t ip = { .addr = client_ip }; | 352 | esp_ip4_addr_t ip = { .addr = client_ip }; |
| 316 | snprintf(resp, sizeof(resp), "mac=" IPSTR, IP2STR(&ip)); | 353 | if (firewall_get_mac_for_ip(client_ip, mac, sizeof(mac)) == ESP_OK) { |
| 354 | snprintf(resp, sizeof(resp), "ip=" IPSTR " mac=%s", IP2STR(&ip), mac); | ||
| 355 | } else { | ||
| 356 | snprintf(resp, sizeof(resp), "ip=" IPSTR " mac=unknown", IP2STR(&ip)); | ||
| 357 | } | ||
| 317 | } else { | 358 | } else { |
| 318 | snprintf(resp, sizeof(resp), "mac=unknown"); | 359 | snprintf(resp, sizeof(resp), "ip=unknown mac=unknown"); |
| 319 | } | 360 | } |
| 320 | httpd_resp_set_type(req, "text/plain"); | 361 | httpd_resp_set_type(req, "text/plain"); |
| 321 | httpd_resp_send(req, resp, strlen(resp)); | 362 | httpd_resp_send(req, resp, strlen(resp)); |