#include "captive_portal.h" #include "firewall.h" #include "session.h" #include "config.h" #include "mint_health.h" #include "esp_log.h" #include "esp_wifi.h" #include "cJSON.h" #include "lwip/sockets.h" #include "lwip/netdb.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include static const char *TAG = "captive_portal"; static httpd_handle_t s_server = NULL; static char s_ap_ip_str[16] = "10.0.0.1"; static const char PORTAL_HTML_TEMPLATE[] = \ "" "" "" "" "TollGate" "" "" "
" "

TollGate

" "

Pay for internet access with ecash

" "
" "
__PRICE__
" "
sats per minute
" "
" "" "" "
" "
SUPPORTED MINTS
" "
__MINT_LIST__
" "
Tap to copy • Green = reachable
" "
" "
" "
" "" ""; static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out) { int sockfd = httpd_req_to_sockfd(req); struct sockaddr_in addr; socklen_t addr_len = sizeof(addr); if (getpeername(sockfd, (struct sockaddr *)&addr, &addr_len) == 0) { *ip_out = addr.sin_addr.s_addr; return ESP_OK; } return ESP_FAIL; } static esp_err_t portal_handler(httpd_req_t *req); static esp_err_t portal_handler(httpd_req_t *req) { ESP_LOGI(TAG, "GET %s from client", req->uri); httpd_resp_set_type(req, "text/html"); const tollgate_config_t *cfg = tollgate_config_get(); char price_str[16]; snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step); const char *tpl = PORTAL_HTML_TEMPLATE; size_t tpl_len = strlen(tpl); char mint_list_html[4096]; size_t mint_list_cap = sizeof(mint_list_html); size_t mint_list_len = 0; mint_list_html[0] = '\0'; int mint_count = 0; const mint_status_t *mints = mint_health_get_all(&mint_count); for (int i = 0; i < mint_count; i++) { const char *cls = mints[i].reachable ? "green" : "grey"; const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim"; int written = snprintf(mint_list_html + mint_list_len, mint_list_cap - mint_list_len, "
" "" "%s
", mints[i].url, cls, url_cls, mints[i].url); if (written > 0 && (size_t)written < mint_list_cap - mint_list_len) { mint_list_len += (size_t)written; } } if (mint_count == 0) { const tollgate_config_t *cfg = tollgate_config_get(); snprintf(mint_list_html, sizeof(mint_list_html), "
" "%s
", cfg->mint_url); } struct { const char *key; const char *val; } subs[] = { { "__AP_IP__", s_ap_ip_str }, { "__PRICE__", price_str }, { "__MINT_LIST__", mint_list_html }, }; int nsubs = sizeof(subs) / sizeof(subs[0]); size_t extra = 0; for (int i = 0; i < nsubs; i++) { const char *p = tpl; size_t klen = strlen(subs[i].key); while ((p = strstr(p, subs[i].key)) != NULL) { extra += strlen(subs[i].val) - klen; p += klen; } } size_t out_size = tpl_len + extra + 1; char *html = malloc(out_size); if (!html) { httpd_resp_send_500(req); return ESP_OK; } char *out = html; const char *src = tpl; while (*src) { const char *earliest = NULL; int ei = -1; for (int i = 0; i < nsubs; i++) { const char *found = strstr(src, subs[i].key); if (found && (earliest == NULL || found < earliest)) { earliest = found; ei = i; } } if (earliest) { size_t vlen = strlen(subs[ei].val); memcpy(out, src, earliest - src); out += earliest - src; memcpy(out, subs[ei].val, vlen); out += vlen; src = earliest + strlen(subs[ei].key); } else { strcpy(out, src); out += strlen(src); break; } } *out = '\0'; httpd_resp_send(req, html, out - html); free(html); return ESP_OK; } static esp_err_t grant_access_handler(httpd_req_t *req) { uint32_t client_ip; if (get_client_ip(req, &client_ip) == ESP_OK) { firewall_grant_access(client_ip); } const char *resp = "{\"status\":\"granted\"}"; httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } static esp_err_t status_handler(httpd_req_t *req) { const tollgate_config_t *cfg = tollgate_config_get(); cJSON *root = cJSON_CreateObject(); cJSON_AddBoolToObject(root, "connected", true); cJSON_AddNumberToObject(root, "price", cfg->price_per_step); char *json = cJSON_PrintUnformatted(root); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, strlen(json)); cJSON_free(json); cJSON_Delete(root); return ESP_OK; } static esp_err_t whoami_handler(httpd_req_t *req) { uint32_t client_ip; char resp[96]; if (get_client_ip(req, &client_ip) == ESP_OK) { char mac[18] = {0}; esp_ip4_addr_t ip = { .addr = client_ip }; if (firewall_get_mac_for_ip(client_ip, mac, sizeof(mac)) == ESP_OK) { snprintf(resp, sizeof(resp), "ip=" IPSTR " mac=%s", IP2STR(&ip), mac); } else { snprintf(resp, sizeof(resp), "ip=" IPSTR " mac=unknown", IP2STR(&ip)); } } else { snprintf(resp, sizeof(resp), "ip=unknown mac=unknown"); } httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } static esp_err_t usage_handler(httpd_req_t *req) { uint32_t client_ip; if (get_client_ip(req, &client_ip) != ESP_OK) { httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, "-1/-1", 5); return ESP_OK; } session_t *session = session_find_by_ip(client_ip); if (!session || !session->active) { httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, "-1/-1", 5); return ESP_OK; } const tollgate_config_t *cfg = tollgate_config_get(); bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); char resp[64]; if (is_bytes) { int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed; if (remaining < 0) remaining = 0; snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_bytes); } else { int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; int64_t remaining = session->allotment_ms - elapsed; if (remaining < 0) remaining = 0; snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); } httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } static esp_err_t reset_auth_handler(httpd_req_t *req) { session_revoke_all(); firewall_revoke_all(); const char *resp = "{\"status\":\"reset\"}"; httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } static esp_err_t redirect_to_portal_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Captive detect: GET %s → 200 portal HTML", req->uri); return portal_handler(req); } static esp_err_t catchall_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Catchall: GET %s → 302 → http://%s/", req->uri, s_ap_ip_str); httpd_resp_set_status(req, "302 Found"); char location[64]; snprintf(location, sizeof(location), "http://%s/", s_ap_ip_str); httpd_resp_set_hdr(req, "Location", location); httpd_resp_set_hdr(req, "Connection", "close"); httpd_resp_send(req, NULL, 0); return ESP_OK; } static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler }; static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler }; static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler }; static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler }; static const httpd_uri_t uri_reset = { .uri = "/reset_authentication", .method = HTTP_GET, .handler = reset_auth_handler }; static const httpd_uri_t uri_gen204 = { .uri = "/generate_204", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_hotspot = { .uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_canonical = { .uri = "/canonical.html", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_success = { .uri = "/success.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_ncsi = { .uri = "/ncsi.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_connecttest = { .uri = "/connecttest.txt", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_wpad = { .uri = "/wpad.dat", .method = HTTP_GET, .handler = redirect_to_portal_handler }; static const httpd_uri_t uri_catchall = { .uri = "/*", .method = HTTP_GET, .handler = catchall_handler }; esp_err_t captive_portal_start(const char *ap_ip_str) { if (s_server) return ESP_OK; strncpy(s_ap_ip_str, ap_ip_str, sizeof(s_ap_ip_str) - 1); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_uri_handlers = 20; config.uri_match_fn = httpd_uri_match_wildcard; esp_err_t ret = httpd_start(&s_server, &config); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret)); return ret; } httpd_register_uri_handler(s_server, &uri_portal); httpd_register_uri_handler(s_server, &uri_grant); httpd_register_uri_handler(s_server, &uri_status); httpd_register_uri_handler(s_server, &uri_whoami); httpd_register_uri_handler(s_server, &uri_usage); httpd_register_uri_handler(s_server, &uri_reset); httpd_register_uri_handler(s_server, &uri_gen204); httpd_register_uri_handler(s_server, &uri_hotspot); httpd_register_uri_handler(s_server, &uri_canonical); httpd_register_uri_handler(s_server, &uri_success); httpd_register_uri_handler(s_server, &uri_ncsi); httpd_register_uri_handler(s_server, &uri_connecttest); httpd_register_uri_handler(s_server, &uri_wpad); httpd_register_uri_handler(s_server, &uri_catchall); ESP_LOGI(TAG, "Captive portal started on port 80"); return ESP_OK; } void captive_portal_stop(void) { if (s_server) { httpd_stop(s_server); s_server = NULL; } } httpd_handle_t captive_portal_get_server(void) { return s_server; }