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.c224
1 files changed, 224 insertions, 0 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
new file mode 100644
index 0000000..acff9c2
--- /dev/null
+++ b/main/captive_portal.c
@@ -0,0 +1,224 @@
1#include "captive_portal.h"
2#include "firewall.h"
3#include "config.h"
4#include "esp_log.h"
5#include "esp_wifi.h"
6#include "cJSON.h"
7#include "lwip/sockets.h"
8#include "lwip/netdb.h"
9#include <string.h>
10#include <sys/param.h>
11
12static const char *TAG = "captive_portal";
13static httpd_handle_t s_server = NULL;
14
15static const char PORTAL_HTML[] = \
16"<!DOCTYPE html>"
17"<html><head>"
18"<meta charset='utf-8'>"
19"<meta name='viewport' content='width=device-width, initial-scale=1'>"
20"<title>TollGate</title>"
21"<style>"
22"*{box-sizing:border-box;margin:0;padding:0}"
23"body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
24"background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;"
25"min-height:100vh;padding:20px}"
26".card{background:#1a1a1a;border:1px solid #333;border-radius:16px;padding:32px;"
27"max-width:400px;width:100%;text-align:center}"
28"h1{font-size:28px;margin-bottom:8px;color:#f7931a}"
29".subtitle{color:#888;margin-bottom:24px;font-size:14px}"
30".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:24px}"
31".price-amount{font-size:36px;font-weight:bold;color:#f7931a}"
32".price-unit{color:#888;font-size:14px}"
33"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
34"#status.success{display:block;background:#1a472a;color:#4caf50}"
35"#status.error{display:block;background:#471a1a;color:#f44336}"
36"#status.processing{display:block;background:#1a3a47;color:#2196f3}"
37".btn{background:#f7931a;color:#000;border:none;border-radius:8px;padding:14px 28px;"
38"font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px}"
39".btn:hover{background:#e8850f}"
40".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
41"</style>"
42"</head><body>"
43"<div class='card'>"
44"<h1>TollGate</h1>"
45"<p class='subtitle'>Pay for internet access with ecash</p>"
46"<div class='price'>"
47"<div class='price-amount' id='price'>Loading...</div>"
48"<div class='price-unit'>sats per minute</div>"
49"</div>"
50"<button class='btn' id='grantBtn' onclick='grantAccess()'>Grant Free Access</button>"
51"<div id='status'></div>"
52"</div>"
53"<script>"
54"const priceEl=document.getElementById('price');"
55"const statusEl=document.getElementById('status');"
56"const grantBtn=document.getElementById('grantBtn');"
57"fetch('/api/status').then(r=>r.json()).then(d=>{priceEl.textContent=d.price||'21';}).catch(()=>{priceEl.textContent='21';});"
58"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
59"function grantAccess(){"
60" grantBtn.disabled=true;"
61" showStatus('Connecting...','processing');"
62" fetch('/grant_access').then(r=>r.json()).then(d=>{"
63" if(d.status==='granted'){"
64" showStatus('Connected! You have internet access.','success');"
65" grantBtn.textContent='Connected!';"
66" setTimeout(()=>{window.location.href='http://detectportal.firefox.com/success.txt';},2000);"
67" }else{showStatus('Error: '+d.message,'error');grantBtn.disabled=false;}"
68" }).catch(e=>{showStatus('Connection error','error');grantBtn.disabled=false;});"
69"}"
70"</script>"
71"</body></html>";
72
73static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out)
74{
75 int sockfd = httpd_req_to_sockfd(req);
76 struct sockaddr_in addr;
77 socklen_t addr_len = sizeof(addr);
78 if (getpeername(sockfd, (struct sockaddr *)&addr, &addr_len) == 0) {
79 *ip_out = addr.sin_addr.s_addr;
80 return ESP_OK;
81 }
82 return ESP_FAIL;
83}
84
85static bool is_captive_detection_uri(const char *uri)
86{
87 return strcmp(uri, "/generate_204") == 0 ||
88 strcmp(uri, "/hotspot-detect.html") == 0 ||
89 strcmp(uri, "/canonical.html") == 0 ||
90 strcmp(uri, "/success.txt") == 0 ||
91 strcmp(uri, "/ncsi.txt") == 0 ||
92 strcmp(uri, "/connecttest.txt") == 0 ||
93 strcmp(uri, "/wpad.dat") == 0 ||
94 strcmp(uri, "/redirect") == 0;
95}
96
97static esp_err_t portal_handler(httpd_req_t *req)
98{
99 httpd_resp_set_type(req, "text/html");
100 httpd_resp_send(req, PORTAL_HTML, strlen(PORTAL_HTML));
101 return ESP_OK;
102}
103
104static esp_err_t grant_access_handler(httpd_req_t *req)
105{
106 uint32_t client_ip;
107 if (get_client_ip(req, &client_ip) == ESP_OK) {
108 firewall_grant_access(client_ip);
109 }
110 const char *resp = "{\"status\":\"granted\"}";
111 httpd_resp_set_type(req, "application/json");
112 httpd_resp_send(req, resp, strlen(resp));
113 return ESP_OK;
114}
115
116static esp_err_t status_handler(httpd_req_t *req)
117{
118 const tollgate_config_t *cfg = tollgate_config_get();
119 cJSON *root = cJSON_CreateObject();
120 cJSON_AddBoolToObject(root, "connected", true);
121 cJSON_AddNumberToObject(root, "price", cfg->price_per_step);
122 char *json = cJSON_PrintUnformatted(root);
123 httpd_resp_set_type(req, "application/json");
124 httpd_resp_send(req, json, strlen(json));
125 cJSON_free(json);
126 cJSON_Delete(root);
127 return ESP_OK;
128}
129
130static esp_err_t whoami_handler(httpd_req_t *req)
131{
132 uint32_t client_ip;
133 char resp[64];
134 if (get_client_ip(req, &client_ip) == ESP_OK) {
135 esp_ip4_addr_t ip = { .addr = client_ip };
136 snprintf(resp, sizeof(resp), "mac=" IPSTR, IP2STR(&ip));
137 } else {
138 snprintf(resp, sizeof(resp), "mac=unknown");
139 }
140 httpd_resp_set_type(req, "text/plain");
141 httpd_resp_send(req, resp, strlen(resp));
142 return ESP_OK;
143}
144
145static esp_err_t usage_handler(httpd_req_t *req)
146{
147 uint32_t client_ip;
148 char resp[32];
149 if (get_client_ip(req, &client_ip) == ESP_OK && firewall_is_client_allowed(client_ip)) {
150 snprintf(resp, sizeof(resp), "0/0");
151 } else {
152 snprintf(resp, sizeof(resp), "-1/-1");
153 }
154 httpd_resp_set_type(req, "text/plain");
155 httpd_resp_send(req, resp, strlen(resp));
156 return ESP_OK;
157}
158
159static esp_err_t reset_auth_handler(httpd_req_t *req)
160{
161 firewall_revoke_all();
162 const char *resp = "{\"status\":\"reset\"}";
163 httpd_resp_set_type(req, "application/json");
164 httpd_resp_send(req, resp, strlen(resp));
165 return ESP_OK;
166}
167
168static esp_err_t catchall_handler(httpd_req_t *req)
169{
170 if (is_captive_detection_uri(req->uri)) {
171 return portal_handler(req);
172 }
173 httpd_resp_set_status(req, "302 Found");
174 httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
175 httpd_resp_send(req, NULL, 0);
176 return ESP_OK;
177}
178
179static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler };
180static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler };
181static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler };
182static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler };
183static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler };
184static const httpd_uri_t uri_reset = { .uri = "/reset_authentication", .method = HTTP_GET, .handler = reset_auth_handler };
185static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler };
186
187esp_err_t captive_portal_start(void)
188{
189 if (s_server) return ESP_OK;
190
191 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
192 config.max_uri_handlers = 10;
193 config.uri_match_fn = httpd_uri_match_wildcard;
194
195 esp_err_t ret = httpd_start(&s_server, &config);
196 if (ret != ESP_OK) {
197 ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
198 return ret;
199 }
200
201 httpd_register_uri_handler(s_server, &uri_portal);
202 httpd_register_uri_handler(s_server, &uri_grant);
203 httpd_register_uri_handler(s_server, &uri_status);
204 httpd_register_uri_handler(s_server, &uri_whoami);
205 httpd_register_uri_handler(s_server, &uri_usage);
206 httpd_register_uri_handler(s_server, &uri_reset);
207 httpd_register_uri_handler(s_server, &uri_catchall);
208
209 ESP_LOGI(TAG, "Captive portal started on port 80");
210 return ESP_OK;
211}
212
213void captive_portal_stop(void)
214{
215 if (s_server) {
216 httpd_stop(s_server);
217 s_server = NULL;
218 }
219}
220
221httpd_handle_t captive_portal_get_server(void)
222{
223 return s_server;
224}