diff options
| author | Your Name <you@example.com> | 2026-05-15 22:27:14 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-15 22:27:14 +0530 |
| commit | 1263d86314fc0760d9be8eea415ccecbc047a5eb (patch) | |
| tree | 778130f0beb59d52f68e0e5f11388bf4b1470130 /main | |
| parent | a7d0a672d59bf8985a6fc0e61b49015fabd96513 (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')
| -rw-r--r-- | main/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | main/captive_portal.c | 35 | ||||
| -rw-r--r-- | main/cashu.c | 241 | ||||
| -rw-r--r-- | main/cashu.h | 42 | ||||
| -rw-r--r-- | main/session.c | 164 | ||||
| -rw-r--r-- | main/session.h | 44 | ||||
| -rw-r--r-- | main/tollgate_api.c | 355 | ||||
| -rw-r--r-- | main/tollgate_api.h | 10 | ||||
| -rw-r--r-- | main/tollgate_main.c | 10 |
9 files changed, 891 insertions, 13 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index a21a53f..97b4c37 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -3,6 +3,9 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 3 | "dns_server.c" | 3 | "dns_server.c" |
| 4 | "captive_portal.c" | 4 | "captive_portal.c" |
| 5 | "firewall.c" | 5 | "firewall.c" |
| 6 | "cashu.c" | ||
| 7 | "session.c" | ||
| 8 | "tollgate_api.c" | ||
| 6 | INCLUDE_DIRS "." "${IDF_PATH}/components/lwip/include/apps" | 9 | INCLUDE_DIRS "." "${IDF_PATH}/components/lwip/include/apps" |
| 7 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 10 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 8 | lwip json esp_http_client mbedtls log spiffs | 11 | lwip json esp_http_client mbedtls log spiffs |
diff --git a/main/captive_portal.c b/main/captive_portal.c index acff9c2..17f672f 100644 --- a/main/captive_portal.c +++ b/main/captive_portal.c | |||
| @@ -38,6 +38,8 @@ static const char PORTAL_HTML[] = \ | |||
| 38 | "font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}" | 38 | "font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}" |
| 39 | ".btn:hover{background:#e8850f}" | 39 | ".btn:hover{background:#e8850f}" |
| 40 | ".btn:disabled{background:#333;color:#666;cursor:not-allowed}" | 40 | ".btn:disabled{background:#333;color:#666;cursor:not-allowed}" |
| 41 | "textarea{width:100%;height:80px;background:#252525;border:1px solid #333;border-radius:8px;" | ||
| 42 | "color:#fff;padding:12px;font-family:monospace;font-size:12px;margin-top:8px;resize:none}" | ||
| 41 | "</style>" | 43 | "</style>" |
| 42 | "</head><body>" | 44 | "</head><body>" |
| 43 | "<div class='card'>" | 45 | "<div class='card'>" |
| @@ -47,25 +49,32 @@ static const char PORTAL_HTML[] = \ | |||
| 47 | "<div class='price-amount' id='price'>Loading...</div>" | 49 | "<div class='price-amount' id='price'>Loading...</div>" |
| 48 | "<div class='price-unit'>sats per minute</div>" | 50 | "<div class='price-unit'>sats per minute</div>" |
| 49 | "</div>" | 51 | "</div>" |
| 50 | "<button class='btn' id='grantBtn' onclick='grantAccess()'>Grant Free Access</button>" | 52 | "<textarea id='tokenInput' placeholder='Paste your Cashu token here (cashuA...)'></textarea>" |
| 53 | "<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" | ||
| 51 | "<div id='status'></div>" | 54 | "<div id='status'></div>" |
| 52 | "</div>" | 55 | "</div>" |
| 53 | "<script>" | 56 | "<script>" |
| 54 | "const priceEl=document.getElementById('price');" | 57 | "const priceEl=document.getElementById('price');" |
| 55 | "const statusEl=document.getElementById('status');" | 58 | "const statusEl=document.getElementById('status');" |
| 56 | "const grantBtn=document.getElementById('grantBtn');" | 59 | "const payBtn=document.getElementById('payBtn');" |
| 57 | "fetch('/api/status').then(r=>r.json()).then(d=>{priceEl.textContent=d.price||'21';}).catch(()=>{priceEl.textContent='21';});" | 60 | "const tokenInput=document.getElementById('tokenInput');" |
| 61 | "fetch('http://192.168.4.1:2121/').then(r=>r.json()).then(d=>{" | ||
| 62 | "if(d.tags){const p=d.tags.find(t=>t[0]==='price_per_step');if(p)priceEl.textContent=p[2]||'21';}" | ||
| 63 | "}).catch(()=>{priceEl.textContent='21';});" | ||
| 58 | "function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" | 64 | "function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" |
| 59 | "function grantAccess(){" | 65 | "function payToken(){" |
| 60 | " grantBtn.disabled=true;" | 66 | "const token=tokenInput.value.trim();" |
| 61 | " showStatus('Connecting...','processing');" | 67 | "if(!token||!token.startsWith('cashuA')){showStatus('Please paste a valid Cashu token','error');return;}" |
| 62 | " fetch('/grant_access').then(r=>r.json()).then(d=>{" | 68 | "payBtn.disabled=true;" |
| 63 | " if(d.status==='granted'){" | 69 | "showStatus('Processing payment...','processing');" |
| 64 | " showStatus('Connected! You have internet access.','success');" | 70 | "fetch('http://192.168.4.1:2121/',{method:'POST',body:token}).then(r=>{" |
| 65 | " grantBtn.textContent='Connected!';" | 71 | "if(r.ok)return r.json();" |
| 66 | " setTimeout(()=>{window.location.href='http://detectportal.firefox.com/success.txt';},2000);" | 72 | "return r.json().then(d=>{throw new Error(d.content||'Payment failed');});" |
| 67 | " }else{showStatus('Error: '+d.message,'error');grantBtn.disabled=false;}" | 73 | "}).then(d=>{" |
| 68 | " }).catch(e=>{showStatus('Connection error','error');grantBtn.disabled=false;});" | 74 | "if(d.kind===1022){showStatus('Connected! You have internet access.','success');payBtn.textContent='Connected!';" |
| 75 | "setTimeout(()=>{window.location.href='http://detectportal.firefox.com/success.txt';},2000);}" | ||
| 76 | "else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" | ||
| 77 | "}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" | ||
| 69 | "}" | 78 | "}" |
| 70 | "</script>" | 79 | "</script>" |
| 71 | "</body></html>"; | 80 | "</body></html>"; |
diff --git a/main/cashu.c b/main/cashu.c new file mode 100644 index 0000000..bafd000 --- /dev/null +++ b/main/cashu.c | |||
| @@ -0,0 +1,241 @@ | |||
| 1 | #include "cashu.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "esp_http_client.h" | ||
| 5 | #include "cJSON.h" | ||
| 6 | #include "mbedtls/base64.h" | ||
| 7 | #include "mbedtls/sha256.h" | ||
| 8 | #include <string.h> | ||
| 9 | |||
| 10 | static const char *TAG = "cashu"; | ||
| 11 | |||
| 12 | static const char V3_PREFIX[] = "cashuA"; | ||
| 13 | static const size_t V3_PREFIX_LEN = 6; | ||
| 14 | |||
| 15 | static int b64url_decode(const char *input, size_t input_len, char *out, size_t out_size, size_t *out_len) | ||
| 16 | { | ||
| 17 | char b64[1024]; | ||
| 18 | size_t b64_len = input_len; | ||
| 19 | if (b64_len >= sizeof(b64)) return -1; | ||
| 20 | memcpy(b64, input, b64_len); | ||
| 21 | b64[b64_len] = '\0'; | ||
| 22 | |||
| 23 | for (size_t i = 0; i < b64_len; i++) { | ||
| 24 | if (b64[i] == '-') b64[i] = '+'; | ||
| 25 | else if (b64[i] == '_') b64[i] = '/'; | ||
| 26 | } | ||
| 27 | while (b64_len % 4 != 0 && b64_len < sizeof(b64) - 1) { | ||
| 28 | b64[b64_len++] = '='; | ||
| 29 | } | ||
| 30 | b64[b64_len] = '\0'; | ||
| 31 | |||
| 32 | size_t olen = 0; | ||
| 33 | int ret = mbedtls_base64_decode((unsigned char *)out, out_size, &olen, | ||
| 34 | (const unsigned char *)b64, b64_len); | ||
| 35 | if (ret != 0) return -1; | ||
| 36 | *out_len = olen; | ||
| 37 | return 0; | ||
| 38 | } | ||
| 39 | |||
| 40 | static esp_err_t parse_proofs_array(cJSON *arr, cashu_token_t *out) | ||
| 41 | { | ||
| 42 | if (!cJSON_IsArray(arr)) return ESP_FAIL; | ||
| 43 | int count = cJSON_GetArraySize(arr); | ||
| 44 | if (count > CASHU_MAX_PROOFS) return ESP_FAIL; | ||
| 45 | |||
| 46 | out->proof_count = 0; | ||
| 47 | out->total_amount = 0; | ||
| 48 | for (int i = 0; i < count; i++) { | ||
| 49 | cJSON *proof = cJSON_GetArrayItem(arr, i); | ||
| 50 | cJSON *amt = cJSON_GetObjectItemCaseSensitive(proof, "amount"); | ||
| 51 | cJSON *id = cJSON_GetObjectItemCaseSensitive(proof, "id"); | ||
| 52 | cJSON *secret = cJSON_GetObjectItemCaseSensitive(proof, "secret"); | ||
| 53 | cJSON *c = cJSON_GetObjectItemCaseSensitive(proof, "C"); | ||
| 54 | |||
| 55 | if (!amt || !cJSON_IsNumber(amt)) return ESP_FAIL; | ||
| 56 | |||
| 57 | out->proofs[i].amount = (uint64_t)amt->valuedouble; | ||
| 58 | out->total_amount += out->proofs[i].amount; | ||
| 59 | |||
| 60 | if (id && cJSON_IsString(id)) { | ||
| 61 | strncpy(out->proofs[i].id, id->valuestring, sizeof(out->proofs[i].id) - 1); | ||
| 62 | } | ||
| 63 | if (secret && cJSON_IsString(secret)) { | ||
| 64 | strncpy(out->proofs[i].secret, secret->valuestring, sizeof(out->proofs[i].secret) - 1); | ||
| 65 | } | ||
| 66 | if (c && cJSON_IsString(c)) { | ||
| 67 | strncpy(out->proofs[i].c, c->valuestring, sizeof(out->proofs[i].c) - 1); | ||
| 68 | } | ||
| 69 | out->proof_count++; | ||
| 70 | } | ||
| 71 | return ESP_OK; | ||
| 72 | } | ||
| 73 | |||
| 74 | esp_err_t cashu_decode_token(const char *token_str, cashu_token_t *out) | ||
| 75 | { | ||
| 76 | if (!token_str || !out) return ESP_FAIL; | ||
| 77 | memset(out, 0, sizeof(*out)); | ||
| 78 | |||
| 79 | size_t len = strlen(token_str); | ||
| 80 | if (len <= V3_PREFIX_LEN) { | ||
| 81 | ESP_LOGE(TAG, "Token too short"); | ||
| 82 | return ESP_FAIL; | ||
| 83 | } | ||
| 84 | if (strncmp(token_str, V3_PREFIX, V3_PREFIX_LEN) != 0) { | ||
| 85 | ESP_LOGE(TAG, "Token missing cashuA prefix"); | ||
| 86 | return ESP_FAIL; | ||
| 87 | } | ||
| 88 | |||
| 89 | char json_buf[2048]; | ||
| 90 | size_t json_len = 0; | ||
| 91 | if (b64url_decode(token_str + V3_PREFIX_LEN, len - V3_PREFIX_LEN, | ||
| 92 | json_buf, sizeof(json_buf) - 1, &json_len) != 0) { | ||
| 93 | ESP_LOGE(TAG, "Base64url decode failed"); | ||
| 94 | return ESP_FAIL; | ||
| 95 | } | ||
| 96 | json_buf[json_len] = '\0'; | ||
| 97 | |||
| 98 | cJSON *root = cJSON_Parse(json_buf); | ||
| 99 | if (!root) { | ||
| 100 | ESP_LOGE(TAG, "JSON parse failed"); | ||
| 101 | return ESP_FAIL; | ||
| 102 | } | ||
| 103 | |||
| 104 | cJSON *token_arr = cJSON_GetObjectItemCaseSensitive(root, "token"); | ||
| 105 | if (token_arr && cJSON_IsArray(token_arr)) { | ||
| 106 | cJSON *first = cJSON_GetArrayItem(token_arr, 0); | ||
| 107 | if (!first) { cJSON_Delete(root); return ESP_FAIL; } | ||
| 108 | |||
| 109 | cJSON *mint = cJSON_GetObjectItemCaseSensitive(first, "mint"); | ||
| 110 | if (mint && cJSON_IsString(mint)) { | ||
| 111 | strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); | ||
| 112 | } | ||
| 113 | |||
| 114 | cJSON *proofs = cJSON_GetObjectItemCaseSensitive(first, "proofs"); | ||
| 115 | if (proofs) { | ||
| 116 | esp_err_t ret = parse_proofs_array(proofs, out); | ||
| 117 | if (ret != ESP_OK) { cJSON_Delete(root); return ret; } | ||
| 118 | } | ||
| 119 | } else { | ||
| 120 | cJSON *mint = cJSON_GetObjectItemCaseSensitive(root, "mint"); | ||
| 121 | if (mint && cJSON_IsString(mint)) { | ||
| 122 | strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); | ||
| 123 | } | ||
| 124 | |||
| 125 | cJSON *proofs = cJSON_GetObjectItemCaseSensitive(root, "proofs"); | ||
| 126 | if (proofs) { | ||
| 127 | esp_err_t ret = parse_proofs_array(proofs, out); | ||
| 128 | if (ret != ESP_OK) { cJSON_Delete(root); return ret; } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | cJSON_Delete(root); | ||
| 133 | |||
| 134 | if (out->proof_count == 0) { | ||
| 135 | ESP_LOGE(TAG, "No proofs in token"); | ||
| 136 | return ESP_FAIL; | ||
| 137 | } | ||
| 138 | |||
| 139 | ESP_LOGI(TAG, "Decoded token: %d proofs, total=%llu, mint=%s", | ||
| 140 | out->proof_count, (unsigned long long)out->total_amount, out->mint_url); | ||
| 141 | return ESP_OK; | ||
| 142 | } | ||
| 143 | |||
| 144 | static void sha256_hex(const char *data, size_t data_len, char *hex_out) | ||
| 145 | { | ||
| 146 | uint8_t hash[32]; | ||
| 147 | mbedtls_sha256((const unsigned char *)data, data_len, hash, 0); | ||
| 148 | for (int i = 0; i < 32; i++) { | ||
| 149 | sprintf(hex_out + i * 2, "%02x", hash[i]); | ||
| 150 | } | ||
| 151 | hex_out[64] = '\0'; | ||
| 152 | } | ||
| 153 | |||
| 154 | esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *token, | ||
| 155 | cashu_proof_state_t *states, int *state_count) | ||
| 156 | { | ||
| 157 | cJSON *ys_arr = cJSON_CreateArray(); | ||
| 158 | for (int i = 0; i < token->proof_count; i++) { | ||
| 159 | char y_hex[65]; | ||
| 160 | sha256_hex(token->proofs[i].secret, strlen(token->proofs[i].secret), y_hex); | ||
| 161 | cJSON_AddItemToArray(ys_arr, cJSON_CreateString(y_hex)); | ||
| 162 | strncpy(states[i].y_hex, y_hex, sizeof(states[i].y_hex) - 1); | ||
| 163 | states[i].spent = false; | ||
| 164 | } | ||
| 165 | *state_count = token->proof_count; | ||
| 166 | |||
| 167 | char *ys_json = cJSON_PrintUnformatted(ys_arr); | ||
| 168 | cJSON_Delete(ys_arr); | ||
| 169 | |||
| 170 | char post_body[2048]; | ||
| 171 | snprintf(post_body, sizeof(post_body), "{\"Ys\":%s}", ys_json); | ||
| 172 | cJSON_free(ys_json); | ||
| 173 | |||
| 174 | char url[512]; | ||
| 175 | snprintf(url, sizeof(url), "%s/v1/checkstate", mint_url); | ||
| 176 | |||
| 177 | char resp_buf[4096]; | ||
| 178 | int resp_len = 0; | ||
| 179 | |||
| 180 | esp_http_client_config_t config = { | ||
| 181 | .url = url, | ||
| 182 | .method = HTTP_METHOD_POST, | ||
| 183 | .timeout_ms = 10000, | ||
| 184 | }; | ||
| 185 | esp_http_client_handle_t client = esp_http_client_init(&config); | ||
| 186 | if (!client) return ESP_FAIL; | ||
| 187 | |||
| 188 | esp_http_client_set_header(client, "Content-Type", "application/json"); | ||
| 189 | esp_err_t err = esp_http_client_open(client, strlen(post_body)); | ||
| 190 | if (err != ESP_OK) { | ||
| 191 | esp_http_client_cleanup(client); | ||
| 192 | return err; | ||
| 193 | } | ||
| 194 | esp_http_client_write(client, post_body, strlen(post_body)); | ||
| 195 | |||
| 196 | resp_len = esp_http_client_read(client, resp_buf, sizeof(resp_buf) - 1); | ||
| 197 | int status = esp_http_client_get_status_code(client); | ||
| 198 | esp_http_client_cleanup(client); | ||
| 199 | |||
| 200 | if (status != 200 || resp_len <= 0) { | ||
| 201 | ESP_LOGE(TAG, "checkstate returned %d", status); | ||
| 202 | return ESP_FAIL; | ||
| 203 | } | ||
| 204 | resp_buf[resp_len] = '\0'; | ||
| 205 | |||
| 206 | cJSON *root = cJSON_Parse(resp_buf); | ||
| 207 | if (!root) return ESP_FAIL; | ||
| 208 | |||
| 209 | cJSON *states_arr = cJSON_GetObjectItemCaseSensitive(root, "states"); | ||
| 210 | if (!states_arr || !cJSON_IsArray(states_arr)) { | ||
| 211 | cJSON_Delete(root); | ||
| 212 | return ESP_FAIL; | ||
| 213 | } | ||
| 214 | |||
| 215 | int n = cJSON_GetArraySize(states_arr); | ||
| 216 | for (int i = 0; i < n && i < token->proof_count; i++) { | ||
| 217 | cJSON *s = cJSON_GetArrayItem(states_arr, i); | ||
| 218 | cJSON *state = cJSON_GetObjectItemCaseSensitive(s, "state"); | ||
| 219 | if (state && cJSON_IsString(state)) { | ||
| 220 | states[i].spent = (strcmp(state->valuestring, "SPENT") == 0); | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | cJSON_Delete(root); | ||
| 225 | return ESP_OK; | ||
| 226 | } | ||
| 227 | |||
| 228 | uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, | ||
| 229 | uint64_t step_size_ms) | ||
| 230 | { | ||
| 231 | if (price_per_step == 0) return 0; | ||
| 232 | return (token_amount / price_per_step) * step_size_ms; | ||
| 233 | } | ||
| 234 | |||
| 235 | bool cashu_is_mint_accepted(const char *mint_url) | ||
| 236 | { | ||
| 237 | if (!mint_url || mint_url[0] == '\0') return false; | ||
| 238 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 239 | if (strstr(mint_url, cfg->mint_url) != NULL) return true; | ||
| 240 | return (strcmp(mint_url, cfg->mint_url) == 0); | ||
| 241 | } | ||
diff --git a/main/cashu.h b/main/cashu.h new file mode 100644 index 0000000..17891c5 --- /dev/null +++ b/main/cashu.h | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | #ifndef CASHU_H | ||
| 2 | #define CASHU_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define CASHU_MAX_PROOFS 10 | ||
| 9 | #define CASHU_MAX_SECRET_LEN 128 | ||
| 10 | #define CASHU_MAX_ID_LEN 16 | ||
| 11 | #define CASHU_MAX_C_LEN 128 | ||
| 12 | |||
| 13 | typedef struct { | ||
| 14 | uint64_t amount; | ||
| 15 | char id[CASHU_MAX_ID_LEN]; | ||
| 16 | char secret[CASHU_MAX_SECRET_LEN]; | ||
| 17 | char c[CASHU_MAX_C_LEN]; | ||
| 18 | } cashu_proof_t; | ||
| 19 | |||
| 20 | typedef struct { | ||
| 21 | cashu_proof_t proofs[CASHU_MAX_PROOFS]; | ||
| 22 | int proof_count; | ||
| 23 | char mint_url[256]; | ||
| 24 | uint64_t total_amount; | ||
| 25 | } cashu_token_t; | ||
| 26 | |||
| 27 | typedef struct { | ||
| 28 | char y_hex[65]; | ||
| 29 | bool spent; | ||
| 30 | } cashu_proof_state_t; | ||
| 31 | |||
| 32 | esp_err_t cashu_decode_token(const char *token_str, cashu_token_t *out); | ||
| 33 | |||
| 34 | esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *token, | ||
| 35 | cashu_proof_state_t *states, int *state_count); | ||
| 36 | |||
| 37 | uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, | ||
| 38 | uint64_t step_size_ms); | ||
| 39 | |||
| 40 | bool cashu_is_mint_accepted(const char *mint_url); | ||
| 41 | |||
| 42 | #endif | ||
diff --git a/main/session.c b/main/session.c new file mode 100644 index 0000000..6d9c334 --- /dev/null +++ b/main/session.c | |||
| @@ -0,0 +1,164 @@ | |||
| 1 | #include "session.h" | ||
| 2 | #include "firewall.h" | ||
| 3 | #include "dns_server.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "freertos/FreeRTOS.h" | ||
| 6 | #include "freertos/task.h" | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | #define SPENT_SECRETS_MAX 100 | ||
| 10 | |||
| 11 | static const char *TAG = "session"; | ||
| 12 | static session_t s_sessions[SESSION_MAX_CLIENTS]; | ||
| 13 | static int s_session_count = 0; | ||
| 14 | |||
| 15 | static char s_spent_secrets[SPENT_SECRETS_MAX][65]; | ||
| 16 | static int s_spent_count = 0; | ||
| 17 | |||
| 18 | static int64_t get_time_ms(void) | ||
| 19 | { | ||
| 20 | return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 21 | } | ||
| 22 | |||
| 23 | esp_err_t session_manager_init(void) | ||
| 24 | { | ||
| 25 | memset(s_sessions, 0, sizeof(s_sessions)); | ||
| 26 | s_session_count = 0; | ||
| 27 | s_spent_count = 0; | ||
| 28 | ESP_LOGI(TAG, "Session manager initialized"); | ||
| 29 | return ESP_OK; | ||
| 30 | } | ||
| 31 | |||
| 32 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, | ||
| 33 | const char *spent_secrets[], int secret_count) | ||
| 34 | { | ||
| 35 | session_t *existing = session_find_by_ip(client_ip); | ||
| 36 | if (existing) { | ||
| 37 | session_extend(existing, allotment_ms); | ||
| 38 | for (int i = 0; i < secret_count && s_spent_count < SPENT_SECRETS_MAX; i++) { | ||
| 39 | strncpy(s_spent_secrets[s_spent_count], spent_secrets[i], 64); | ||
| 40 | s_spent_secrets[s_spent_count][64] = '\0'; | ||
| 41 | s_spent_count++; | ||
| 42 | } | ||
| 43 | return existing; | ||
| 44 | } | ||
| 45 | |||
| 46 | if (s_session_count >= SESSION_MAX_CLIENTS) { | ||
| 47 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 48 | if (!s_sessions[i].active || session_is_expired(&s_sessions[i])) { | ||
| 49 | session_revoke(&s_sessions[i]); | ||
| 50 | break; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 56 | if (!s_sessions[i].active) { | ||
| 57 | s_sessions[i].client_ip = client_ip; | ||
| 58 | s_sessions[i].allotment_ms = allotment_ms; | ||
| 59 | s_sessions[i].start_time_ms = get_time_ms(); | ||
| 60 | s_sessions[i].active = true; | ||
| 61 | s_sessions[i].spent_secret_count = 0; | ||
| 62 | |||
| 63 | for (int j = 0; j < secret_count && j < 5; j++) { | ||
| 64 | strncpy(s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count], | ||
| 65 | spent_secrets[j], 64); | ||
| 66 | s_sessions[i].spent_secrets[s_sessions[i].spent_secret_count][64] = '\0'; | ||
| 67 | s_sessions[i].spent_secret_count++; | ||
| 68 | } | ||
| 69 | |||
| 70 | for (int j = 0; j < secret_count && s_spent_count < SPENT_SECRETS_MAX; j++) { | ||
| 71 | strncpy(s_spent_secrets[s_spent_count], spent_secrets[j], 64); | ||
| 72 | s_spent_secrets[s_spent_count][64] = '\0'; | ||
| 73 | s_spent_count++; | ||
| 74 | } | ||
| 75 | |||
| 76 | s_session_count++; | ||
| 77 | firewall_grant_access(client_ip); | ||
| 78 | |||
| 79 | esp_ip4_addr_t ip = { .addr = client_ip }; | ||
| 80 | ESP_LOGI(TAG, "Session created: " IPSTR " allotment=%llums", IP2STR(&ip), | ||
| 81 | (unsigned long long)allotment_ms); | ||
| 82 | return &s_sessions[i]; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | ESP_LOGW(TAG, "No free session slots"); | ||
| 87 | return NULL; | ||
| 88 | } | ||
| 89 | |||
| 90 | session_t *session_find_by_ip(uint32_t client_ip) | ||
| 91 | { | ||
| 92 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 93 | if (s_sessions[i].active && s_sessions[i].client_ip == client_ip) { | ||
| 94 | return &s_sessions[i]; | ||
| 95 | } | ||
| 96 | } | ||
| 97 | return NULL; | ||
| 98 | } | ||
| 99 | |||
| 100 | void session_extend(session_t *session, uint64_t additional_ms) | ||
| 101 | { | ||
| 102 | if (!session || !session->active) return; | ||
| 103 | session->allotment_ms += additional_ms; | ||
| 104 | esp_ip4_addr_t ip = { .addr = session->client_ip }; | ||
| 105 | ESP_LOGI(TAG, "Session extended: " IPSTR " +%llums (total=%llu)", IP2STR(&ip), | ||
| 106 | (unsigned long long)additional_ms, (unsigned long long)session->allotment_ms); | ||
| 107 | } | ||
| 108 | |||
| 109 | bool session_is_expired(const session_t *session) | ||
| 110 | { | ||
| 111 | if (!session || !session->active) return true; | ||
| 112 | int64_t elapsed = get_time_ms() - session->start_time_ms; | ||
| 113 | return elapsed >= (int64_t)session->allotment_ms; | ||
| 114 | } | ||
| 115 | |||
| 116 | bool session_is_secret_spent(const char *secret) | ||
| 117 | { | ||
| 118 | for (int i = 0; i < s_spent_count; i++) { | ||
| 119 | if (strncmp(s_spent_secrets[i], secret, 64) == 0) return true; | ||
| 120 | } | ||
| 121 | return false; | ||
| 122 | } | ||
| 123 | |||
| 124 | void session_check_expiry(void) | ||
| 125 | { | ||
| 126 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 127 | if (s_sessions[i].active && session_is_expired(&s_sessions[i])) { | ||
| 128 | esp_ip4_addr_t ip = { .addr = s_sessions[i].client_ip }; | ||
| 129 | ESP_LOGI(TAG, "Session expired: " IPSTR, IP2STR(&ip)); | ||
| 130 | session_revoke(&s_sessions[i]); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | void session_revoke(session_t *session) | ||
| 136 | { | ||
| 137 | if (!session || !session->active) return; | ||
| 138 | firewall_revoke_access(session->client_ip); | ||
| 139 | session->active = false; | ||
| 140 | s_session_count--; | ||
| 141 | } | ||
| 142 | |||
| 143 | void session_revoke_all(void) | ||
| 144 | { | ||
| 145 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 146 | if (s_sessions[i].active) { | ||
| 147 | session_revoke(&s_sessions[i]); | ||
| 148 | } | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | int session_active_count(void) | ||
| 153 | { | ||
| 154 | int count = 0; | ||
| 155 | for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { | ||
| 156 | if (s_sessions[i].active) count++; | ||
| 157 | } | ||
| 158 | return count; | ||
| 159 | } | ||
| 160 | |||
| 161 | void session_tick(void) | ||
| 162 | { | ||
| 163 | session_check_expiry(); | ||
| 164 | } | ||
diff --git a/main/session.h b/main/session.h new file mode 100644 index 0000000..e7d78d4 --- /dev/null +++ b/main/session.h | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | #ifndef SESSION_H | ||
| 2 | #define SESSION_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define SESSION_MAX_CLIENTS 10 | ||
| 9 | #define SESSION_MAX_MAC_LEN 18 | ||
| 10 | |||
| 11 | typedef struct { | ||
| 12 | uint32_t client_ip; | ||
| 13 | char mac[SESSION_MAX_MAC_LEN]; | ||
| 14 | uint64_t allotment_ms; | ||
| 15 | int64_t start_time_ms; | ||
| 16 | bool active; | ||
| 17 | char spent_secrets[5][65]; | ||
| 18 | int spent_secret_count; | ||
| 19 | } session_t; | ||
| 20 | |||
| 21 | esp_err_t session_manager_init(void); | ||
| 22 | |||
| 23 | session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, | ||
| 24 | const char *spent_secrets[], int secret_count); | ||
| 25 | |||
| 26 | session_t *session_find_by_ip(uint32_t client_ip); | ||
| 27 | |||
| 28 | void session_extend(session_t *session, uint64_t additional_ms); | ||
| 29 | |||
| 30 | bool session_is_expired(const session_t *session); | ||
| 31 | |||
| 32 | bool session_is_secret_spent(const char *secret); | ||
| 33 | |||
| 34 | void session_check_expiry(void); | ||
| 35 | |||
| 36 | void session_revoke(session_t *session); | ||
| 37 | |||
| 38 | void session_revoke_all(void); | ||
| 39 | |||
| 40 | int session_active_count(void); | ||
| 41 | |||
| 42 | void session_tick(void); | ||
| 43 | |||
| 44 | #endif | ||
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 | |||
| 11 | static const char *TAG = "tollgate_api"; | ||
| 12 | static httpd_handle_t s_api_server = NULL; | ||
| 13 | |||
| 14 | static const char *TOLLGATE_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000"; | ||
| 15 | |||
| 16 | static 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 | |||
| 28 | static 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 | |||
| 47 | static 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 | |||
| 86 | static 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 | |||
| 137 | static 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 | |||
| 282 | static 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 | |||
| 305 | static 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 | |||
| 320 | static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery }; | ||
| 321 | static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment }; | ||
| 322 | static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage }; | ||
| 323 | static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = api_get_whoami }; | ||
| 324 | |||
| 325 | esp_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 | |||
| 349 | void tollgate_api_stop(void) | ||
| 350 | { | ||
| 351 | if (s_api_server) { | ||
| 352 | httpd_stop(s_api_server); | ||
| 353 | s_api_server = NULL; | ||
| 354 | } | ||
| 355 | } | ||
diff --git a/main/tollgate_api.h b/main/tollgate_api.h new file mode 100644 index 0000000..23e0d75 --- /dev/null +++ b/main/tollgate_api.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef TOLLGATE_API_H | ||
| 2 | #define TOLLGATE_API_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include "esp_http_server.h" | ||
| 6 | |||
| 7 | esp_err_t tollgate_api_start(void); | ||
| 8 | void tollgate_api_stop(void); | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 9eba61f..04f64b9 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -14,6 +14,8 @@ | |||
| 14 | #include "dns_server.h" | 14 | #include "dns_server.h" |
| 15 | #include "captive_portal.h" | 15 | #include "captive_portal.h" |
| 16 | #include "firewall.h" | 16 | #include "firewall.h" |
| 17 | #include "session.h" | ||
| 18 | #include "tollgate_api.h" | ||
| 17 | 19 | ||
| 18 | #define MAX_STA_RETRY 5 | 20 | #define MAX_STA_RETRY 5 |
| 19 | #define AP_IP_ADDR "192.168.4.1" | 21 | #define AP_IP_ADDR "192.168.4.1" |
| @@ -105,9 +107,11 @@ static void start_services(void) | |||
| 105 | upstream_dns.addr = dns_addr->addr; | 107 | upstream_dns.addr = dns_addr->addr; |
| 106 | 108 | ||
| 107 | firewall_init(ap_ip_info.ip); | 109 | firewall_init(ap_ip_info.ip); |
| 110 | session_manager_init(); | ||
| 108 | 111 | ||
| 109 | dns_server_start(ap_ip_info.ip, upstream_dns); | 112 | dns_server_start(ap_ip_info.ip, upstream_dns); |
| 110 | captive_portal_start(); | 113 | captive_portal_start(); |
| 114 | tollgate_api_start(); | ||
| 111 | 115 | ||
| 112 | s_services_running = true; | 116 | s_services_running = true; |
| 113 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 117 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| @@ -123,6 +127,7 @@ static void stop_services(void) | |||
| 123 | } | 127 | } |
| 124 | 128 | ||
| 125 | captive_portal_stop(); | 129 | captive_portal_stop(); |
| 130 | tollgate_api_stop(); | ||
| 126 | dns_server_stop(); | 131 | dns_server_stop(); |
| 127 | firewall_disable_nat(); | 132 | firewall_disable_nat(); |
| 128 | firewall_revoke_all(); | 133 | firewall_revoke_all(); |
| @@ -218,4 +223,9 @@ void app_main(void) | |||
| 218 | ESP_ERROR_CHECK(esp_wifi_start()); | 223 | ESP_ERROR_CHECK(esp_wifi_start()); |
| 219 | 224 | ||
| 220 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); | 225 | ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); |
| 226 | |||
| 227 | while (1) { | ||
| 228 | vTaskDelay(pdMS_TO_TICKS(1000)); | ||
| 229 | session_tick(); | ||
| 230 | } | ||
| 221 | } | 231 | } |