upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/captive_portal.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/captive_portal.c')
-rw-r--r--main/captive_portal.c104
1 files changed, 72 insertions, 32 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
index 0ae46ab..1f7340e 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -11,8 +11,9 @@
11 11
12static const char *TAG = "captive_portal"; 12static const char *TAG = "captive_portal";
13static httpd_handle_t s_server = NULL; 13static httpd_handle_t s_server = NULL;
14static char s_ap_ip_str[16] = "10.0.0.1";
14 15
15static const char PORTAL_HTML[] = \ 16static const char PORTAL_HTML_TEMPLATE[] = \
16"<!DOCTYPE html>" 17"<!DOCTYPE html>"
17"<html><head>" 18"<html><head>"
18"<meta charset='utf-8'>" 19"<meta charset='utf-8'>"
@@ -27,9 +28,15 @@ static const char PORTAL_HTML[] = \
27"max-width:400px;width:100%;text-align:center}" 28"max-width:400px;width:100%;text-align:center}"
28"h1{font-size:28px;margin-bottom:8px;color:#f7931a}" 29"h1{font-size:28px;margin-bottom:8px;color:#f7931a}"
29".subtitle{color:#888;margin-bottom:24px;font-size:14px}" 30".subtitle{color:#888;margin-bottom:24px;font-size:14px}"
30".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:24px}" 31".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}"
31".price-amount{font-size:36px;font-weight:bold;color:#f7931a}" 32".price-amount{font-size:36px;font-weight:bold;color:#f7931a}"
32".price-unit{color:#888;font-size:14px}" 33".price-unit{color:#888;font-size:14px}"
34".mints{background:#252525;border-radius:12px;padding:12px;margin-bottom:16px;text-align:left}"
35".mints-title{color:#888;font-size:12px;margin-bottom:8px}"
36".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all;"
37"background:#1a1a1a;padding:8px;border-radius:6px;position:relative;cursor:pointer}"
38".mint-url:active{opacity:0.7}"
39".mint-hint{color:#666;font-size:10px;margin-top:4px}"
33"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}" 40"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
34"#status.success{display:block;background:#1a472a;color:#4caf50}" 41"#status.success{display:block;background:#1a472a;color:#4caf50}"
35"#status.error{display:block;background:#471a1a;color:#f44336}" 42"#status.error{display:block;background:#471a1a;color:#f44336}"
@@ -49,6 +56,11 @@ static const char PORTAL_HTML[] = \
49"<div class='price-amount' id='price'>Loading...</div>" 56"<div class='price-amount' id='price'>Loading...</div>"
50"<div class='price-unit'>sats per minute</div>" 57"<div class='price-unit'>sats per minute</div>"
51"</div>" 58"</div>"
59"<div class='mints'>"
60"<div class='mints-title'>SUPPORTED MINTS</div>"
61"<div class='mint-url' id='mintUrl' onclick='copyMint()'>Loading...</div>"
62"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>"
63"</div>"
52"<textarea id='tokenInput' placeholder='Paste your Cashu token here (cashuA...)'></textarea>" 64"<textarea id='tokenInput' placeholder='Paste your Cashu token here (cashuA...)'></textarea>"
53"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" 65"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>"
54"<div id='status'></div>" 66"<div id='status'></div>"
@@ -58,16 +70,25 @@ static const char PORTAL_HTML[] = \
58"const statusEl=document.getElementById('status');" 70"const statusEl=document.getElementById('status');"
59"const payBtn=document.getElementById('payBtn');" 71"const payBtn=document.getElementById('payBtn');"
60"const tokenInput=document.getElementById('tokenInput');" 72"const tokenInput=document.getElementById('tokenInput');"
61"fetch('http://192.168.4.1:2121/').then(r=>r.json()).then(d=>{" 73"const mintUrlEl=document.getElementById('mintUrl');"
62"if(d.tags){const p=d.tags.find(t=>t[0]==='price_per_step');if(p)priceEl.textContent=p[2]||'21';}" 74"fetch('http://__AP_IP__:2121/').then(r=>r.json()).then(d=>{"
63"}).catch(()=>{priceEl.textContent='21';});" 75"if(d.tags){"
76"const p=d.tags.find(t=>t[0]==='price_per_step');if(p){priceEl.textContent=p[2]||'21';"
77"if(p[4]){mintUrlEl.textContent=p[4];}}"
78"}"
79"}).catch(()=>{priceEl.textContent='21';mintUrlEl.textContent='Error loading mint URL';});"
80"function copyMint(){"
81"const url=mintUrlEl.textContent;"
82"if(navigator.clipboard){navigator.clipboard.writeText(url);"
83"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=url;},1000);}"
84"}"
64"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" 85"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
65"function payToken(){" 86"function payToken(){"
66"const token=tokenInput.value.trim();" 87"const token=tokenInput.value.trim();"
67"if(!token||!token.startsWith('cashuA')){showStatus('Please paste a valid Cashu token','error');return;}" 88"if(!token||!token.startsWith('cashuA')){showStatus('Please paste a valid Cashu token','error');return;}"
68"payBtn.disabled=true;" 89"payBtn.disabled=true;"
69"showStatus('Processing payment...','processing');" 90"showStatus('Processing payment...','processing');"
70"fetch('http://192.168.4.1:2121/',{method:'POST',body:token}).then(r=>{" 91"fetch('http://__AP_IP__:2121/',{method:'POST',body:token}).then(r=>{"
71"if(r.ok)return r.json();" 92"if(r.ok)return r.json();"
72"return r.json().then(d=>{throw new Error(d.content||'Payment failed');});" 93"return r.json().then(d=>{throw new Error(d.content||'Payment failed');});"
73"}).then(d=>{" 94"}).then(d=>{"
@@ -91,28 +112,47 @@ static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out)
91 return ESP_FAIL; 112 return ESP_FAIL;
92} 113}
93 114
94static bool is_captive_detection_uri(const char *uri) 115static esp_err_t portal_handler(httpd_req_t *req);
95{
96 return strcmp(uri, "/generate_204") == 0 ||
97 strcmp(uri, "/hotspot-detect.html") == 0 ||
98 strcmp(uri, "/canonical.html") == 0 ||
99 strcmp(uri, "/success.txt") == 0 ||
100 strcmp(uri, "/ncsi.txt") == 0 ||
101 strcmp(uri, "/connecttest.txt") == 0 ||
102 strcmp(uri, "/wpad.dat") == 0 ||
103 strcmp(uri, "/redirect") == 0 ||
104 strcmp(uri, "/kindle-wifi/wifistub.html") == 0 ||
105 strcmp(uri, "/fwlink") == 0 ||
106 strcmp(uri, "/connectivity-check.html") == 0 ||
107 strcmp(uri, "/generate_204/") == 0 ||
108 strcmp(uri, "/hotspot-detect.html/") == 0;
109}
110 116
111static esp_err_t portal_handler(httpd_req_t *req) 117static esp_err_t portal_handler(httpd_req_t *req)
112{ 118{
113 ESP_LOGI(TAG, "GET %s from client", req->uri); 119 ESP_LOGI(TAG, "GET %s from client", req->uri);
114 httpd_resp_set_type(req, "text/html"); 120 httpd_resp_set_type(req, "text/html");
115 httpd_resp_send(req, PORTAL_HTML, strlen(PORTAL_HTML)); 121
122 char *html = NULL;
123 const char *tpl = PORTAL_HTML_TEMPLATE;
124 size_t tpl_len = strlen(tpl);
125 int count = 0;
126 const char *p = tpl;
127 while ((p = strstr(p, "__AP_IP__")) != NULL) { count++; p += 9; }
128
129 size_t ip_len = strlen(s_ap_ip_str);
130 html = malloc(tpl_len + count * (ip_len > 9 ? ip_len - 9 : 0) + 1);
131 if (!html) {
132 httpd_resp_send_500(req);
133 return ESP_OK;
134 }
135
136 char *out = html;
137 const char *src = tpl;
138 while (*src) {
139 const char *found = strstr(src, "__AP_IP__");
140 if (found) {
141 memcpy(out, src, found - src);
142 out += found - src;
143 memcpy(out, s_ap_ip_str, ip_len);
144 out += ip_len;
145 src = found + 9;
146 } else {
147 strcpy(out, src);
148 out += strlen(src);
149 break;
150 }
151 }
152 *out = '\0';
153
154 httpd_resp_send(req, html, out - html);
155 free(html);
116 return ESP_OK; 156 return ESP_OK;
117} 157}
118 158
@@ -187,19 +227,18 @@ static esp_err_t reset_auth_handler(httpd_req_t *req)
187 227
188static esp_err_t redirect_to_portal_handler(httpd_req_t *req) 228static esp_err_t redirect_to_portal_handler(httpd_req_t *req)
189{ 229{
190 ESP_LOGI(TAG, "Captive detect: GET %s → 302 → http://192.168.4.1/", req->uri); 230 ESP_LOGI(TAG, "Captive detect: GET %s → 200 portal HTML", req->uri);
191 httpd_resp_set_status(req, "302 Found"); 231 return portal_handler(req);
192 httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
193 httpd_resp_set_hdr(req, "Connection", "close");
194 httpd_resp_send(req, NULL, 0);
195 return ESP_OK;
196} 232}
197 233
198static esp_err_t catchall_handler(httpd_req_t *req) 234static esp_err_t catchall_handler(httpd_req_t *req)
199{ 235{
200 ESP_LOGI(TAG, "Catchall: GET %s → 302 → http://192.168.4.1/", req->uri); 236 ESP_LOGI(TAG, "Catchall: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str);
201 httpd_resp_set_status(req, "302 Found"); 237 httpd_resp_set_status(req, "302 Found");
202 httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/"); 238
239 char location[64];
240 snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str);
241 httpd_resp_set_hdr(req, "Location", location);
203 httpd_resp_set_hdr(req, "Connection", "close"); 242 httpd_resp_set_hdr(req, "Connection", "close");
204 httpd_resp_send(req, NULL, 0); 243 httpd_resp_send(req, NULL, 0);
205 return ESP_OK; 244 return ESP_OK;
@@ -220,9 +259,10 @@ static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method
220static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; 259static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler };
221static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler }; 260static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler };
222 261
223esp_err_t captive_portal_start(void) 262esp_err_t captive_portal_start(const char *ap_ip_str)
224{ 263{
225 if (s_server) return ESP_OK; 264 if (s_server) return ESP_OK;
265 strncpy(s_ap_ip_str, ap_ip_str, sizeof(s_ap_ip_str) - 1);
226 266
227 httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 267 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
228 config.max_uri_handlers = 20; 268 config.max_uri_handlers = 20;