Files

262 lines
9.9 KiB
C

#include "maze32_network.h"
#include <stdio.h>
#include <string.h>
#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;
}