#include "captive_portal.h" #include "wifi_cfg.h" #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_http_server.h" #include "esp_wifi.h" #include "esp_system.h" #include "lwip/sockets.h" #define DNS_PORT 53 #define HTTP_PORT 80 #define MAX_SCAN_AP 20 static httpd_handle_t s_httpd; static TaskHandle_t s_dns_task; static int s_dns_sock = -1; static bool s_active; static const uint8_t AP_IP[4] = {192, 168, 4, 1}; /* ------------------------------------------------------------------ */ /* Config page HTML */ /* ------------------------------------------------------------------ */ static const char CONFIG_PAGE[] = "" "" "" "EIS4 Setup" "" "

EIS4 Setup

" "
" "" "
  • Scanning...
" "" "" "" "" "" "" "" "

Minimum 8 characters, or leave blank for open network

" "" "
" "
" ""; /* ------------------------------------------------------------------ */ /* URL parameter helpers */ /* ------------------------------------------------------------------ */ static void url_decode(char *dst, const char *src, size_t dst_sz) { size_t di = 0; while (*src && di < dst_sz - 1) { if (*src == '%' && src[1] && src[2]) { char hex[3] = {src[1], src[2], 0}; dst[di++] = (char)strtol(hex, NULL, 16); src += 3; } else if (*src == '+') { dst[di++] = ' '; src++; } else { dst[di++] = *src++; } } dst[di] = '\0'; } static void get_param(const char *body, const char *key, char *val, size_t val_sz) { val[0] = '\0'; size_t klen = strlen(key); const char *p = body; while ((p = strstr(p, key)) != NULL) { if ((p == body || *(p - 1) == '&') && p[klen] == '=') { p += klen + 1; const char *end = strchr(p, '&'); size_t len = end ? (size_t)(end - p) : strlen(p); char raw[128]; if (len >= sizeof(raw)) len = sizeof(raw) - 1; memcpy(raw, p, len); raw[len] = '\0'; url_decode(val, raw, val_sz); return; } p++; } } /* ------------------------------------------------------------------ */ /* JSON escape for SSIDs */ /* ------------------------------------------------------------------ */ static int json_escape(char *dst, size_t dst_sz, const char *src) { size_t di = 0; while (*src && di < dst_sz - 2) { if (*src == '"' || *src == '\\') { if (di + 2 >= dst_sz) break; dst[di++] = '\\'; } dst[di++] = *src++; } dst[di] = '\0'; return (int)di; } /* ------------------------------------------------------------------ */ /* HTTP handlers */ /* ------------------------------------------------------------------ */ static esp_err_t handle_root(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); httpd_resp_send(req, CONFIG_PAGE, HTTPD_RESP_USE_STRLEN); return ESP_OK; } static esp_err_t handle_scan(httpd_req_t *req) { wifi_scan_config_t scan_cfg = {0}; esp_wifi_scan_start(&scan_cfg, true); uint16_t count = 0; esp_wifi_scan_get_ap_num(&count); if (count > MAX_SCAN_AP) count = MAX_SCAN_AP; wifi_ap_record_t *aps = calloc(count ? count : 1, sizeof(wifi_ap_record_t)); if (!aps) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, NULL); return ESP_FAIL; } esp_wifi_scan_get_ap_records(&count, aps); char json[2048]; int off = 0; off += snprintf(json + off, sizeof(json) - off, "["); for (int i = 0; i < count && off < (int)sizeof(json) - 80; i++) { if (!aps[i].ssid[0]) continue; /* skip duplicates (scan is sorted by rssi, first is strongest) */ bool dup = false; for (int j = 0; j < i; j++) { if (strcmp((char *)aps[i].ssid, (char *)aps[j].ssid) == 0) { dup = true; break; } } if (dup) continue; char escaped[66]; json_escape(escaped, sizeof(escaped), (char *)aps[i].ssid); if (off > 1) off += snprintf(json + off, sizeof(json) - off, ","); off += snprintf(json + off, sizeof(json) - off, "{\"s\":\"%s\",\"r\":%d}", escaped, aps[i].rssi); } off += snprintf(json + off, sizeof(json) - off, "]"); free(aps); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, off); return ESP_OK; } static esp_err_t handle_save(httpd_req_t *req) { char buf[256]; int n = httpd_req_recv(req, buf, sizeof(buf) - 1); if (n <= 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); return ESP_FAIL; } buf[n] = '\0'; char ssid[WIFI_CFG_SSID_MAX] = {0}; char pass[WIFI_CFG_PASS_MAX] = {0}; char ap_pass[WIFI_CFG_PASS_MAX] = {0}; get_param(buf, "ssid", ssid, sizeof(ssid)); get_param(buf, "pass", pass, sizeof(pass)); get_param(buf, "ap_pass", ap_pass, sizeof(ap_pass)); if (!ssid[0]) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing SSID"); return ESP_FAIL; } if (ap_pass[0] && strlen(ap_pass) < 8) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "AP password must be 8+ characters or empty"); return ESP_FAIL; } printf("Portal: saving STA \"%s\", AP pass %s\n", ssid, ap_pass[0] ? "set" : "open"); wifi_cfg_set_sta(ssid, pass); wifi_cfg_set_ap("EIS4", ap_pass); httpd_resp_sendstr(req, "OK"); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); return ESP_OK; } static esp_err_t handle_catchall(httpd_req_t *req) { httpd_resp_set_status(req, "302 Found"); httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/"); httpd_resp_send(req, NULL, 0); return ESP_OK; } /* ------------------------------------------------------------------ */ /* DNS redirect server */ /* ------------------------------------------------------------------ */ static void dns_task(void *arg) { (void)arg; s_dns_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s_dns_sock < 0) { printf("DNS: socket failed\n"); vTaskDelete(NULL); return; } struct sockaddr_in bind_addr = { .sin_family = AF_INET, .sin_port = htons(DNS_PORT), .sin_addr.s_addr = htonl(INADDR_ANY), }; if (bind(s_dns_sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { printf("DNS: bind failed\n"); close(s_dns_sock); s_dns_sock = -1; vTaskDelete(NULL); return; } printf("DNS: redirecting all queries to " "%d.%d.%d.%d\n", AP_IP[0], AP_IP[1], AP_IP[2], AP_IP[3]); uint8_t buf[256]; struct sockaddr_in src; socklen_t slen; for (;;) { slen = sizeof(src); int n = recvfrom(s_dns_sock, buf, sizeof(buf), 0, (struct sockaddr *)&src, &slen); if (n < 12) { if (n < 0) break; continue; } /* walk question section to find its end */ int qend = 12; bool valid = true; while (qend < n && buf[qend] != 0) { uint8_t ll = buf[qend]; if (ll > 63 || qend + ll + 1 > n) { valid = false; break; } qend += ll + 1; } if (!valid) continue; qend += 5; /* null terminator + QTYPE(2) + QCLASS(2) */ if (qend > n) continue; /* build response: header + question (copied) + one A answer */ uint8_t rsp[280]; if (qend + 16 > (int)sizeof(rsp)) continue; memcpy(rsp, buf, qend); rsp[2] = 0x81; rsp[3] = 0x80; /* flags: standard response */ rsp[6] = 0x00; rsp[7] = 0x01; /* 1 answer RR */ int off = qend; rsp[off++] = 0xC0; rsp[off++] = 0x0C; /* name pointer */ rsp[off++] = 0x00; rsp[off++] = 0x01; /* type A */ rsp[off++] = 0x00; rsp[off++] = 0x01; /* class IN */ rsp[off++] = 0x00; rsp[off++] = 0x00; rsp[off++] = 0x00; rsp[off++] = 0x01; /* TTL 1s */ rsp[off++] = 0x00; rsp[off++] = 0x04; /* rdlength */ rsp[off++] = AP_IP[0]; rsp[off++] = AP_IP[1]; rsp[off++] = AP_IP[2]; rsp[off++] = AP_IP[3]; sendto(s_dns_sock, rsp, off, 0, (struct sockaddr *)&src, slen); } vTaskDelete(NULL); } /* ------------------------------------------------------------------ */ /* Public API */ /* ------------------------------------------------------------------ */ esp_err_t captive_portal_start(void) { if (s_active) return ESP_OK; httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.server_port = HTTP_PORT; cfg.stack_size = 6144; cfg.uri_match_fn = httpd_uri_match_wildcard; cfg.max_uri_handlers = 8; esp_err_t err = httpd_start(&s_httpd, &cfg); if (err != ESP_OK) { printf("Portal: HTTP start failed: %d\n", err); return err; } const httpd_uri_t uri_scan = {"/scan", HTTP_GET, handle_scan, NULL}; const httpd_uri_t uri_save = {"/save", HTTP_POST, handle_save, NULL}; const httpd_uri_t uri_root = {"/", HTTP_GET, handle_root, NULL}; const httpd_uri_t uri_catch = {"/*", HTTP_GET, handle_catchall, NULL}; httpd_register_uri_handler(s_httpd, &uri_scan); httpd_register_uri_handler(s_httpd, &uri_save); httpd_register_uri_handler(s_httpd, &uri_root); httpd_register_uri_handler(s_httpd, &uri_catch); xTaskCreate(dns_task, "dns", 3072, NULL, 5, &s_dns_task); s_active = true; printf("Portal: active on port %d\n", HTTP_PORT); return ESP_OK; } esp_err_t captive_portal_stop(void) { if (!s_active) return ESP_OK; if (s_httpd) { httpd_stop(s_httpd); s_httpd = NULL; } if (s_dns_sock >= 0) { close(s_dns_sock); s_dns_sock = -1; } s_active = false; printf("Portal: stopped\n"); return ESP_OK; } bool captive_portal_is_active(void) { return s_active; }