diff --git a/components/network_provisioning/src/manager.c b/components/network_provisioning/src/manager.c index f0d023f..15125a0 100644 --- a/components/network_provisioning/src/manager.c +++ b/components/network_provisioning/src/manager.c @@ -314,11 +314,8 @@ static cJSON *network_prov_get_info_json(void) #ifdef CONFIG_NETWORK_PROV_NETWORK_TYPE_WIFI /* Indicate capability for performing Wi-Fi provision */ cJSON_AddItemToArray(prov_capabilities, cJSON_CreateString("wifi_prov")); - /* Keep Wi-Fi scan capability for compatibility with provisioning apps - * that require it in their save/apply flow, even when scan results may be empty. */ - cJSON_AddItemToArray(prov_capabilities, cJSON_CreateString("wifi_scan")); - /* Indicate that client should allow manual SSID/password entry instead of - * relying on AP scan results. */ + /* Manual-entry provisioning mode: do not advertise wifi_scan capability + * because scan may return an empty list in SoftAP provisioning scenarios. */ cJSON_AddItemToArray(prov_capabilities, cJSON_CreateString("no_scan")); #endif #ifdef CONFIG_NETWORK_PROV_NETWORK_TYPE_THREAD diff --git a/main/modules/wifi_provisioning/wifi_provisioning.cpp b/main/modules/wifi_provisioning/wifi_provisioning.cpp index d946f59..3d12b89 100644 --- a/main/modules/wifi_provisioning/wifi_provisioning.cpp +++ b/main/modules/wifi_provisioning/wifi_provisioning.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -408,28 +409,206 @@ static void wifi_prov_print_url(const char *name, /* Respond to Android captive-portal probes (any GET) with HTTP 204 so Android * considers the SoftAP network valid and does not drop the connection before - * the provisioning app has a chance to exchange credentials. */ + * the provisioning app has a chance to exchange credentials. + * + * For the manual fallback flow, return a redirect to /diag so the phone opens + * the local credential page instead of silently treating the network as normal. */ static esp_err_t prov_captive_portal_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Captive portal probe intercepted: %s", req->uri); - /* 204 No Content: tells Android/iOS the network has internet access. */ - httpd_resp_set_status(req, "204 No Content"); - httpd_resp_send(req, NULL, 0); + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", "/diag"); + httpd_resp_set_type(req, "text/html"); + httpd_resp_sendstr(req, + "" + "Open /diag to continue Wi-Fi setup."); return ESP_OK; } static esp_err_t prov_diag_get_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Provisioning diag GET hit: %s", req->uri); - httpd_resp_set_type(req, "application/json"); - return httpd_resp_sendstr(req, "{\"ok\":true,\"service\":\"provisioning-httpd\"}"); + static const char diag_html[] = + "" + "" + "ESP32 Wi-Fi Setup" + "" + "

ESP32 Manual Wi-Fi Provisioning

" + "

If app provisioning fails, use this form.

" + "
" + "" + "" + "" + "
" + "

Or POST JSON to / or /diag with {"ssid":"...","password":"..."}.

" + ""; + + httpd_resp_set_type(req, "text/html"); + return httpd_resp_send(req, diag_html, HTTPD_RESP_USE_STRLEN); +} + +static esp_err_t wifi_apply_manual_credentials(const char *ssid, const char *password) +{ + if(ssid == NULL || ssid[0] == '\0') { + return ESP_ERR_INVALID_ARG; + } + + const size_t ssid_len = strnlen(ssid, sizeof(((wifi_sta_config_t *)0)->ssid)); + const size_t pass_len = (password != NULL) ? strnlen(password, sizeof(((wifi_sta_config_t *)0)->password)) : 0; + if(ssid_len == 0 || ssid_len >= sizeof(((wifi_sta_config_t *)0)->ssid)) { + return ESP_ERR_INVALID_ARG; + } + if(pass_len >= sizeof(((wifi_sta_config_t *)0)->password)) { + return ESP_ERR_INVALID_ARG; + } + + wifi_config_t sta_cfg = {0}; + memcpy(sta_cfg.sta.ssid, ssid, ssid_len); + if(password != NULL && password[0] != '\0') { + strlcpy((char *)sta_cfg.sta.password, password, sizeof(sta_cfg.sta.password)); + } + + esp_err_t ret = esp_wifi_set_mode(WIFI_MODE_APSTA); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "Manual credential apply: failed to set APSTA mode (err=%d)", ret); + return ret; + } + + ret = esp_wifi_set_storage(WIFI_STORAGE_FLASH); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "Manual credential apply: failed to set Wi-Fi storage (err=%d)", ret); + return ret; + } + + ret = esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "Manual credential apply: failed to set STA config (err=%d)", ret); + return ret; + } + + /* Manual provisioning fallback bypasses protocomm flow, so enable normal STA + * reconnect behavior immediately. */ + wifi_is_provisioning = false; + + ret = esp_wifi_connect(); + if(ret != ESP_OK) { + ESP_LOGE(TAG, "Manual credential apply: failed to trigger STA connect (err=%d)", ret); + return ret; + } + + ESP_LOGI(TAG, "Manual credential apply accepted for SSID: %s", ssid); + return ESP_OK; +} + +static bool diag_parse_form_field(const char *body, const char *key, char *out, size_t out_len) +{ + if(body == NULL || key == NULL || out == NULL || out_len == 0) { + return false; + } + + const size_t key_len = strlen(key); + const char *match = strstr(body, key); + if(match == NULL || match[key_len] != '=') { + return false; + } + + match += key_len + 1; + size_t w = 0; + while(*match != '\0' && *match != '&' && w + 1 < out_len) { + if(*match == '+') { + out[w++] = ' '; + } else if(*match == '%' && isxdigit((unsigned char)match[1]) && isxdigit((unsigned char)match[2])) { + char hex[3] = { match[1], match[2], '\0' }; + out[w++] = (char)strtol(hex, NULL, 16); + match += 2; + } else { + out[w++] = *match; + } + match++; + } + out[w] = '\0'; + return w > 0 || (key_len > 0 && strcmp(key, "password") == 0); } static esp_err_t prov_diag_post_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Provisioning diag POST hit: %s (len=%d)", req->uri, req->content_len); + + if(req->content_len <= 0 || req->content_len > 512) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_sendstr(req, "{\"ok\":false,\"error\":\"invalid_content_length\"}"); + } + + char body[513] = {0}; + int total_read = 0; + while(total_read < req->content_len) { + int r = httpd_req_recv(req, body + total_read, req->content_len - total_read); + if(r <= 0) { + httpd_resp_set_status(req, "408 Request Timeout"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_sendstr(req, "{\"ok\":false,\"error\":\"recv_failed\"}"); + } + total_read += r; + } + body[total_read] = '\0'; + + char ssid[33] = {0}; + char password[65] = {0}; + const char *ssid_ptr = NULL; + const char *password_ptr = NULL; + + cJSON *root = cJSON_Parse(body); + if(root != NULL) { + const cJSON *ssid_item = cJSON_GetObjectItemCaseSensitive(root, "ssid"); + const cJSON *password_item = cJSON_GetObjectItemCaseSensitive(root, "password"); + if(password_item == NULL) { + password_item = cJSON_GetObjectItemCaseSensitive(root, "passphrase"); + } + if(cJSON_IsString(ssid_item)) { + ssid_ptr = ssid_item->valuestring; + } + if(cJSON_IsString(password_item)) { + password_ptr = password_item->valuestring; + } + cJSON_Delete(root); + } + + if(ssid_ptr == NULL) { + if(!diag_parse_form_field(body, "ssid", ssid, sizeof(ssid))) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_sendstr(req, "{\"ok\":false,\"error\":\"missing_ssid\"}"); + } + ssid_ptr = ssid; + } + if(password_ptr == NULL) { + if(diag_parse_form_field(body, "password", password, sizeof(password))) { + password_ptr = password; + } else if(diag_parse_form_field(body, "passphrase", password, sizeof(password))) { + password_ptr = password; + } else { + password_ptr = ""; + } + } + + esp_err_t apply_ret = wifi_apply_manual_credentials(ssid_ptr, password_ptr); + + if(apply_ret != ESP_OK) { + char resp[96] = {0}; + snprintf(resp, sizeof(resp), "{\"ok\":false,\"error\":\"apply_failed\",\"code\":%d}", (int)apply_ret); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_sendstr(req, resp); + } + httpd_resp_set_type(req, "application/json"); - return httpd_resp_sendstr(req, "{\"ok\":true,\"method\":\"post\"}"); + return httpd_resp_sendstr(req, "{\"ok\":true,\"mode\":\"manual_wifi_apply\"}"); } static esp_err_t prov_httpd_err_handler(httpd_req_t *req, httpd_err_code_t error) @@ -449,6 +628,20 @@ static esp_err_t prov_httpd_err_handler(httpd_req_t *req, httpd_err_code_t error return ESP_FAIL; } +static const httpd_uri_t prov_root_get_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = prov_diag_get_handler, + .user_ctx = NULL +}; + +static const httpd_uri_t prov_root_post_uri = { + .uri = "/", + .method = HTTP_POST, + .handler = prov_diag_post_handler, + .user_ctx = NULL +}; + static const httpd_uri_t prov_captive_portal_uri = { .uri = "/*", /* wildcard: catches /generate_204 and friends */ .method = HTTP_GET, @@ -588,6 +781,7 @@ static esp_err_t wifi_start_softap_provisioning(void) security_ver = 0; security_params = NULL; security_label = "sec0"; + pop = NULL; #else #error "At least one protocomm security version must be enabled" #endif diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 8a4a6d8..2b732e9 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -34,3 +34,4 @@ CONFIG_LV_USE_QRCODE=y # CONFIG_LV_USE_DEMO_MUSIC is not set CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1=y CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0=y