#include "maze32_network.h" #include #include #include "esp_log.h" #include "maze32_provisioning_protocol.h" #define MAZE32_PROVISION_BODY_LIMIT 256U #define MAZE32_PROVISION_RESPONSE_LIMIT 2048U static const char *TAG = "maze32_provision"; static const char *PROVISION_PATH = "/littlefs/provision.html.gz"; static int append_json_string(char *output, size_t capacity, size_t *length, const char *value) { if (*length + 1U >= capacity) { return 0; } output[(*length)++] = '"'; for (const unsigned char *cursor = (const unsigned char *)value; *cursor != '\0'; ++cursor) { const char *escape = NULL; if (*cursor == '"') { escape = "\\\""; } else if (*cursor == '\\') { escape = "\\\\"; } else if (*cursor == '\b') { escape = "\\b"; } else if (*cursor == '\f') { escape = "\\f"; } else if (*cursor == '\n') { escape = "\\n"; } else if (*cursor == '\r') { escape = "\\r"; } else if (*cursor == '\t') { escape = "\\t"; } if (escape != NULL) { if (*length + 2U >= capacity) { return 0; } output[(*length)++] = escape[0]; output[(*length)++] = escape[1]; } else { if (*cursor < 0x20U) { if (*length + 6U >= capacity) { return 0; } int written = snprintf(output + *length, capacity - *length, "\\u%04x", *cursor); if (written != 6) { return 0; } *length += 6U; continue; } if (*length + 1U >= capacity) { return 0; } output[(*length)++] = (char)*cursor; } } if (*length + 1U >= capacity) { return 0; } output[(*length)++] = '"'; output[*length] = '\0'; return 1; } static esp_err_t send_json(httpd_req_t *request, const char *status, const char *body) { httpd_resp_set_status(request, status); httpd_resp_set_type(request, "application/json"); httpd_resp_set_hdr(request, "Cache-Control", "no-store"); return httpd_resp_sendstr(request, body); } static esp_err_t send_provision_page(httpd_req_t *request) { FILE *file = fopen(PROVISION_PATH, "rb"); if (file == NULL) { ESP_LOGE(TAG, "Provisioning asset is unavailable: %s", PROVISION_PATH); return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Provisioning application is unavailable"); } httpd_resp_set_type(request, "text/html; charset=utf-8"); httpd_resp_set_hdr(request, "Content-Encoding", "gzip"); httpd_resp_set_hdr(request, "Cache-Control", "no-cache"); char buffer[1024]; esp_err_t result = ESP_OK; size_t bytes_read; while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0U) { result = httpd_resp_send_chunk(request, buffer, bytes_read); if (result != ESP_OK) { break; } } if (ferror(file) != 0) { result = ESP_FAIL; } fclose(file); return result == ESP_OK ? httpd_resp_send_chunk(request, NULL, 0) : result; } static esp_err_t send_status(httpd_req_t *request) { maze32_network_t *network = request->user_ctx; char address[16] = ""; if (maze32_network_state(network) == MAZE32_NETWORK_STATION_CONNECTED) { maze32_network_ip_address(network, address, sizeof(address)); } char response[256]; int length = snprintf(response, sizeof(response), "{\"state\":\"%s\",\"accessPointSsid\":\"%s\",\"ip\":\"%s\"}", maze32_network_state_name(maze32_network_state(network)), maze32_network_access_point_ssid(network), address); if (length < 0 || (size_t)length >= sizeof(response)) { return ESP_FAIL; } return send_json(request, "200 OK", response); } static esp_err_t send_scan(httpd_req_t *request) { maze32_network_t *network = request->user_ctx; maze32_wifi_scan_result_t results[MAZE32_WIFI_SCAN_LIMIT]; size_t count = 0U; esp_err_t result = maze32_network_scan(network, results, MAZE32_WIFI_SCAN_LIMIT, &count); if (result != ESP_OK) { return send_json(request, "503 Service Unavailable", "{\"error\":\"Сканирование сейчас недоступно\"}"); } char response[MAZE32_PROVISION_RESPONSE_LIMIT] = "{\"networks\":["; size_t length = strlen(response); for (size_t index = 0U; index < count; ++index) { const char *prefix = index == 0U ? "{" : ",{"; int written = snprintf(response + length, sizeof(response) - length, "%s\"ssid\":", prefix); if (written < 0 || (size_t)written >= sizeof(response) - length) { return ESP_ERR_NO_MEM; } length += (size_t)written; if (!append_json_string(response, sizeof(response), &length, results[index].ssid)) { return ESP_ERR_NO_MEM; } written = snprintf(response + length, sizeof(response) - length, ",\"rssi\":%d,\"secure\":%s}", results[index].rssi, results[index].auth_mode == 0U ? "false" : "true"); if (written < 0 || (size_t)written >= sizeof(response) - length) { return ESP_ERR_NO_MEM; } length += (size_t)written; } int written = snprintf(response + length, sizeof(response) - length, "]}"); if (written < 0 || (size_t)written >= sizeof(response) - length) { return ESP_ERR_NO_MEM; } return send_json(request, "200 OK", response); } static esp_err_t read_credentials(httpd_req_t *request, maze32_wifi_credentials_t *credentials) { if (request->content_len == 0U || request->content_len > MAZE32_PROVISION_BODY_LIMIT) { return ESP_ERR_INVALID_SIZE; } char body[MAZE32_PROVISION_BODY_LIMIT + 1U]; size_t received = 0U; while (received < request->content_len) { int current = httpd_req_recv(request, body + received, request->content_len - received); if (current <= 0) { return ESP_FAIL; } received += (size_t)current; } body[received] = '\0'; if (!maze32_provisioning_parse_credentials(body, received, credentials)) { return ESP_ERR_INVALID_ARG; } return ESP_OK; } static esp_err_t apply_credentials(httpd_req_t *request) { maze32_network_t *network = request->user_ctx; maze32_wifi_credentials_t credentials; esp_err_t result = read_credentials(request, &credentials); if (result != ESP_OK) { return send_json(request, "400 Bad Request", "{\"error\":\"Проверьте имя сети и пароль\"}"); } result = maze32_network_apply_credentials(network, &credentials); memset(&credentials, 0, sizeof(credentials)); if (result == ESP_ERR_INVALID_STATE) { return send_json(request, "409 Conflict", "{\"error\":\"Сначала завершите текущую игру\"}"); } if (result != ESP_OK) { return send_json(request, "500 Internal Server Error", "{\"error\":\"Не удалось сохранить настройки\"}"); } result = httpd_queue_work(network->server, maze32_network_retry_saved_credentials, network); if (result != ESP_OK) { return send_json(request, "500 Internal Server Error", "{\"error\":\"Настройки сохранены; перезагрузите устройство\"}"); } return send_json(request, "202 Accepted", "{\"status\":\"saved\",\"message\":\"Подключаемся к сети…\"}"); } static esp_err_t reset_credentials(httpd_req_t *request) { maze32_network_t *network = request->user_ctx; esp_err_t result = maze32_network_reset_credentials(network); if (result == ESP_ERR_INVALID_STATE) { return send_json(request, "409 Conflict", "{\"error\":\"Сначала завершите текущую игру\"}"); } if (result != ESP_OK) { return send_json(request, "500 Internal Server Error", "{\"error\":\"Не удалось сбросить настройки\"}"); } result = httpd_queue_work(network->server, maze32_network_return_to_provisioning, network); if (result != ESP_OK) { return ESP_FAIL; } return send_json(request, "202 Accepted", "{\"status\":\"reset\",\"message\":\"Настройки сети удалены\"}"); } esp_err_t maze32_provisioning_register(httpd_handle_t server, maze32_network_t *network) { if (server == NULL || network == NULL) { return ESP_ERR_INVALID_ARG; } network->server = server; const httpd_uri_t routes[] = { {.uri = "/provision", .method = HTTP_GET, .handler = send_provision_page, .user_ctx = network}, {.uri = "/api/wifi/status", .method = HTTP_GET, .handler = send_status, .user_ctx = network}, {.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = send_scan, .user_ctx = network}, {.uri = "/api/wifi/config", .method = HTTP_POST, .handler = apply_credentials, .user_ctx = network}, {.uri = "/api/wifi/reset", .method = HTTP_POST, .handler = reset_credentials, .user_ctx = network}, }; for (size_t index = 0U; index < sizeof(routes) / sizeof(routes[0]); ++index) { esp_err_t result = httpd_register_uri_handler(server, &routes[index]); if (result != ESP_OK) { return result; } } ESP_LOGI(TAG, "Provisioning endpoints ready"); return ESP_OK; }