447 lines
22 KiB
C
447 lines
22 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
#include "web_network_settings.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "admin_ssh_console.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "mdns_service.h"
|
|
#include "secure_random.h"
|
|
#include "web_cookie_auth.h"
|
|
#include "web_httpd_adapter.h"
|
|
#include "wifi_manager.h"
|
|
|
|
enum { WIFI_PATCH, PROFILE_PATCH, WIFI_SAVE, WIFI_LOAD, START, STOP, RECONNECT,
|
|
NEXT_PROFILE, MDNS_SET, MDNS_SAVE, MDNS_LOAD, MDNS_DEFAULTS, ACTION_COUNT };
|
|
static const char *const s_actions[] = {"wifi-patch", "profile-patch", "wifi-save", "wifi-load",
|
|
"start", "stop", "reconnect", "next-profile", "mdns-set", "mdns-save", "mdns-load", "mdns-defaults"};
|
|
enum { IDLE, PENDING, ACCEPTED, OK, FAILED, CANCELLED, STALE, INVALID,
|
|
LOADED_DEFAULTS, APPLIED_NOT_QUEUED };
|
|
static const char *const s_states[] = {"idle", "pending", "accepted", "ok", "failed", "cancelled",
|
|
"stale", "invalid", "loaded_defaults", "applied_not_queued"};
|
|
typedef struct {
|
|
uint32_t id, generation;
|
|
web_session_id_t session;
|
|
user_principal_t principal;
|
|
int64_t deadline;
|
|
unsigned action, state;
|
|
esp_err_t error;
|
|
bool executing;
|
|
wifi_manager_patch_t patch;
|
|
mdns_config_t mdns;
|
|
} network_operation_t;
|
|
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
|
static network_operation_t s_operation;
|
|
static uint32_t s_next_id;
|
|
static esp_timer_handle_t s_secret_timer;
|
|
static bool s_secret_timer_started;
|
|
|
|
static void wipe_input(network_operation_t *operation)
|
|
{
|
|
secure_wipe(&operation->principal, sizeof(operation->principal));
|
|
secure_wipe(&operation->patch, sizeof(operation->patch));
|
|
secure_wipe(&operation->mdns, sizeof(operation->mdns));
|
|
operation->generation = 0;
|
|
}
|
|
|
|
static void expire_input(void *unused)
|
|
{
|
|
(void)unused;
|
|
int64_t now = esp_timer_get_time();
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_operation.state == PENDING && !s_operation.executing && now >= s_operation.deadline) {
|
|
s_operation.state = CANCELLED;
|
|
wipe_input(&s_operation);
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
|
|
static bool ensure_secret_timer(void)
|
|
{
|
|
/* Sole HTTPD admission owner; one firmware-lifetime timer, no extra task.
|
|
* Periodic inspection avoids an old captured expiry cancelling a newer ID. */
|
|
if (!s_secret_timer) {
|
|
const esp_timer_create_args_t args = {.callback = expire_input, .name = "network-input"};
|
|
if (esp_timer_create(&args, &s_secret_timer) != ESP_OK) return false;
|
|
}
|
|
if (!s_secret_timer_started) {
|
|
if (esp_timer_start_periodic(s_secret_timer, 1000000ULL) != ESP_OK) return false;
|
|
s_secret_timer_started = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
typedef struct { const char *body; size_t size, pos; } parser_t;
|
|
static void space(parser_t *p)
|
|
{
|
|
while (p->pos < p->size && (p->body[p->pos] == ' ' || p->body[p->pos] == '\r' ||
|
|
p->body[p->pos] == '\n' || p->body[p->pos] == '\t')) ++p->pos;
|
|
}
|
|
static bool take(parser_t *p, char c)
|
|
{
|
|
space(p);
|
|
return p->pos < p->size && p->body[p->pos++] == c;
|
|
}
|
|
static int hex_digit(unsigned char c)
|
|
{
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
/* Bounded byte-string decoder, deliberately not Unicode-to-UTF8 conversion.
|
|
* See the public contract: \\u00ff is exactly one SSID byte, not two. */
|
|
static bool byte_string(parser_t *p, uint8_t *out, size_t capacity, size_t *length)
|
|
{
|
|
*length = 0;
|
|
if (!take(p, '"')) return false;
|
|
while (p->pos < p->size) {
|
|
unsigned char c = (unsigned char)p->body[p->pos++];
|
|
if (c == '"') return true;
|
|
if (c < 0x20 || c > 0x7e || *length == capacity) return false;
|
|
if (c == '\\') {
|
|
if (p->pos == p->size) return false;
|
|
c = (unsigned char)p->body[p->pos++];
|
|
switch (c) {
|
|
case '"': case '\\': case '/': break;
|
|
case 'b': c = '\b'; break;
|
|
case 'f': c = '\f'; break;
|
|
case 'n': c = '\n'; break;
|
|
case 'r': c = '\r'; break;
|
|
case 't': c = '\t'; break;
|
|
case 'u': {
|
|
if (p->size - p->pos < 4 || p->body[p->pos] != '0' || p->body[p->pos + 1] != '0') return false;
|
|
int high = hex_digit(p->body[p->pos + 2]), low = hex_digit(p->body[p->pos + 3]);
|
|
if (high < 0 || low < 0) return false;
|
|
c = (unsigned char)(high * 16 + low); p->pos += 4; break;
|
|
}
|
|
default: return false;
|
|
}
|
|
}
|
|
out[(*length)++] = c;
|
|
}
|
|
return false;
|
|
}
|
|
static bool number(parser_t *p, uint32_t *out)
|
|
{
|
|
space(p); size_t start = p->pos; *out = 0;
|
|
while (p->pos < p->size && p->body[p->pos] >= '0' && p->body[p->pos] <= '9') {
|
|
unsigned digit = (unsigned)(p->body[p->pos++] - '0');
|
|
if (*out > (UINT32_MAX - digit) / 10) return false;
|
|
*out = *out * 10 + digit;
|
|
}
|
|
return p->pos > start && (p->pos - start == 1 || p->body[start] != '0');
|
|
}
|
|
static bool boolean(parser_t *p, uint32_t *out)
|
|
{
|
|
space(p);
|
|
if (p->size - p->pos >= 4 && !memcmp(p->body + p->pos, "true", 4)) { p->pos += 4; *out = 1; return true; }
|
|
if (p->size - p->pos >= 5 && !memcmp(p->body + p->pos, "false", 5)) { p->pos += 5; *out = 0; return true; }
|
|
return false;
|
|
}
|
|
|
|
static bool parse_request(const char *body, size_t length, network_operation_t *operation)
|
|
{
|
|
enum { ACTION, GENERATION, PROFILE, ENABLED, PRIORITY, SECURITY, SSID, PASSWORD,
|
|
CLEAR_PASSWORD, BOOT, POLICY, CHANNEL, SUFFIX, KEY_COUNT };
|
|
static const char *const keys[] = {"action", "generation", "profile", "enabled", "priority", "security",
|
|
"ssid", "password", "clear_password", "enabled_at_boot", "ap_policy", "channel", "suffix"};
|
|
parser_t p = {.body = body, .size = length};
|
|
uint32_t seen = 0;
|
|
operation->action = ACTION_COUNT;
|
|
operation->patch.profile = -1;
|
|
if (!take(&p, '{')) return false;
|
|
for (unsigned field = 0; field < KEY_COUNT; ++field) {
|
|
uint8_t key_text[20] = {0}; size_t n;
|
|
if ((field && !take(&p, ',')) || !byte_string(&p, key_text, sizeof(key_text), &n)) return false;
|
|
unsigned key = 0;
|
|
for (; key < KEY_COUNT; ++key) if (strlen(keys[key]) == n && !memcmp(keys[key], key_text, n)) break;
|
|
if (key == KEY_COUNT || (seen & (1U << key)) || !take(&p, ':')) return false;
|
|
seen |= 1U << key;
|
|
uint32_t value = 0;
|
|
uint8_t text[64] = {0};
|
|
bool valid;
|
|
if (key == GENERATION || key == PROFILE || key == PRIORITY || key == CHANNEL) valid = number(&p, &value);
|
|
else if (key == ENABLED || key == CLEAR_PASSWORD || key == BOOT) valid = boolean(&p, &value);
|
|
else valid = byte_string(&p, text, sizeof(text) - 1, &n);
|
|
if (!valid) { secure_wipe(text, sizeof(text)); return false; }
|
|
switch (key) {
|
|
case ACTION:
|
|
for (unsigned i = 0; i < ACTION_COUNT; ++i)
|
|
if (strlen(s_actions[i]) == n && !memcmp(s_actions[i], text, n)) operation->action = i;
|
|
valid = operation->action != ACTION_COUNT; break;
|
|
case GENERATION: operation->generation = value; valid = value != 0; break;
|
|
case PROFILE: valid = value < WIFI_CONFIG_STA_PROFILE_COUNT; operation->patch.profile = (int8_t)value; break;
|
|
case ENABLED: operation->patch.enabled = value; operation->patch.fields |= WIFI_PATCH_ENABLED; break;
|
|
case PRIORITY: valid = value <= UINT8_MAX; operation->patch.priority = value; operation->patch.fields |= WIFI_PATCH_PRIORITY; break;
|
|
case SECURITY:
|
|
valid = !memchr(text, 0, n) && wifi_config_parse_security((char *)text, &operation->patch.security);
|
|
operation->patch.fields |= WIFI_PATCH_SECURITY; break;
|
|
case SSID:
|
|
valid = n <= WIFI_CONFIG_SSID_MAX_LEN;
|
|
if (valid) { memcpy(operation->patch.ssid, text, n); operation->patch.ssid_len = n; }
|
|
operation->patch.fields |= WIFI_PATCH_SSID; break;
|
|
case PASSWORD:
|
|
valid = n >= WIFI_CONFIG_PSK_MIN_LEN && n <= WIFI_CONFIG_PSK_MAX_LEN;
|
|
for (size_t i = 0; valid && i < n; ++i) valid = text[i] >= 0x20 && text[i] <= 0x7e;
|
|
if (valid) { memcpy(operation->patch.password, text, n); operation->patch.password_len = n; }
|
|
operation->patch.fields |= WIFI_PATCH_PASSWORD; break;
|
|
case CLEAR_PASSWORD: valid = value == 1; operation->patch.fields |= WIFI_PATCH_PASSWORD; break;
|
|
case BOOT: operation->patch.enabled_at_boot = value; operation->patch.fields |= WIFI_PATCH_BOOT; break;
|
|
case POLICY:
|
|
valid = !memchr(text, 0, n) && wifi_config_parse_ap_policy((char *)text, &operation->patch.ap_policy);
|
|
operation->patch.fields |= WIFI_PATCH_POLICY; break;
|
|
case CHANNEL: valid = value >= WIFI_CONFIG_AP_CHANNEL_MIN && value <= WIFI_CONFIG_AP_CHANNEL_MAX;
|
|
operation->patch.ap_channel = value; operation->patch.fields |= WIFI_PATCH_CHANNEL; break;
|
|
case SUFFIX:
|
|
valid = n <= MDNS_CONFIG_SUFFIX_MAX_LEN;
|
|
if (valid) {
|
|
operation->mdns.schema_version = MDNS_CONFIG_SCHEMA_VERSION;
|
|
operation->mdns.blob_size = MDNS_CONFIG_BLOB_SIZE;
|
|
operation->mdns.suffix_len = n; memcpy(operation->mdns.suffix, text, n);
|
|
valid = mdns_config_validate(&operation->mdns) == ESP_OK;
|
|
}
|
|
break;
|
|
}
|
|
secure_wipe(text, sizeof(text));
|
|
if (!valid) return false;
|
|
space(&p);
|
|
if (p.pos < p.size && p.body[p.pos] == '}') break;
|
|
}
|
|
if (!take(&p, '}')) return false;
|
|
space(&p);
|
|
if (p.pos != p.size || !(seen & 1U) ||
|
|
((seen & (1U << PASSWORD)) && (seen & (1U << CLEAR_PASSWORD)))) return false;
|
|
uint32_t required = 1U, allowed = 1U;
|
|
if (operation->action == WIFI_PATCH || operation->action == PROFILE_PATCH) {
|
|
required |= 1U << GENERATION;
|
|
allowed = required | (1U << SSID) | (1U << PASSWORD) | (1U << CLEAR_PASSWORD);
|
|
if (operation->action == PROFILE_PATCH) {
|
|
required |= 1U << PROFILE;
|
|
allowed |= (1U << PROFILE) | (1U << ENABLED) | (1U << PRIORITY) | (1U << SECURITY);
|
|
} else allowed |= (1U << BOOT) | (1U << POLICY) | (1U << CHANNEL);
|
|
if (!operation->patch.fields) return false;
|
|
} else if (operation->action == WIFI_SAVE || operation->action == WIFI_LOAD || operation->action >= MDNS_SET) {
|
|
required |= 1U << GENERATION;
|
|
if (operation->action == MDNS_SET) required |= 1U << SUFFIX;
|
|
allowed = required;
|
|
}
|
|
return operation->action < ACTION_COUNT && (seen & required) == required && !(seen & ~allowed);
|
|
}
|
|
|
|
void web_network_settings_execute(uint32_t id)
|
|
{
|
|
network_operation_t operation = {0};
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool claimed = id && s_operation.id == id && s_operation.state == PENDING && !s_operation.executing;
|
|
if (claimed) {
|
|
s_operation.executing = true;
|
|
operation = s_operation;
|
|
wipe_input(&s_operation);
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!claimed) return;
|
|
bool current = false;
|
|
esp_err_t error = web_session_store_check_principal(operation.session, &operation.principal, ¤t);
|
|
unsigned state = CANCELLED;
|
|
if (error == ESP_OK && current && operation.principal.role == USER_ROLE_ADMIN &&
|
|
esp_timer_get_time() < operation.deadline) {
|
|
state = ACCEPTED;
|
|
switch (operation.action) {
|
|
case WIFI_PATCH: case PROFILE_PATCH: error = wifi_manager_patch_current(operation.generation, &operation.patch); break;
|
|
case WIFI_SAVE: error = wifi_manager_save_current(operation.generation); state = OK; break;
|
|
case WIFI_LOAD: error = wifi_manager_load_current(operation.generation); break;
|
|
case START: error = wifi_manager_start(); break;
|
|
case STOP: error = wifi_manager_stop(); break;
|
|
case RECONNECT: error = wifi_manager_reconnect(); break;
|
|
case NEXT_PROFILE: error = wifi_manager_next_profile(); break;
|
|
default: {
|
|
bool stored = true;
|
|
mdns_settings_action_t action = operation.action == MDNS_SET ? MDNS_SETTINGS_SET :
|
|
operation.action == MDNS_SAVE ? MDNS_SETTINGS_SAVE :
|
|
operation.action == MDNS_LOAD ? MDNS_SETTINGS_LOAD : MDNS_SETTINGS_DEFAULTS;
|
|
error = mdns_service_update_current(operation.generation, action, &operation.mdns, &stored);
|
|
if (error == ESP_OK) {
|
|
if (action == MDNS_SETTINGS_SAVE) state = OK;
|
|
else {
|
|
error = wifi_manager_mdns_reannounce();
|
|
state = error != ESP_OK ? APPLIED_NOT_QUEUED : stored ? ACCEPTED : LOADED_DEFAULTS;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (error != ESP_OK && state != APPLIED_NOT_QUEUED)
|
|
state = error == ESP_ERR_NOT_FOUND ? STALE : error == ESP_ERR_INVALID_ARG ? INVALID : FAILED;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_operation.id == id && s_operation.state == PENDING) {
|
|
s_operation.state = state;
|
|
s_operation.error = error;
|
|
s_operation.executing = false;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
secure_wipe(&operation, sizeof(operation));
|
|
}
|
|
|
|
static esp_err_t respond(httpd_req_t *request, const char *status, const char *body)
|
|
{
|
|
esp_err_t error = httpd_resp_set_status(request, status);
|
|
if (error == ESP_OK) error = httpd_resp_set_type(request, "application/json; charset=utf-8");
|
|
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
|
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
|
|
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "Referrer-Policy", "no-referrer");
|
|
if (error == ESP_OK) error = httpd_resp_sendstr(request, body);
|
|
return web_httpd_unread_body(request) ? ESP_FAIL : error;
|
|
}
|
|
static esp_err_t authorize(httpd_req_t *request, bool mutation, web_session_view_t *view, bool *allowed)
|
|
{
|
|
esp_err_t error = mutation ? web_cookie_auth_require_json(request, WEB_NETWORK_REQUEST_MAX, view, allowed) :
|
|
web_cookie_auth_require(request, false, false, view, allowed);
|
|
if (error == ESP_OK && *allowed && view->principal.role != USER_ROLE_ADMIN) {
|
|
*allowed = false;
|
|
error = respond(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static bool append(char *out, size_t capacity, size_t *used, const char *format, ...)
|
|
{
|
|
va_list args; va_start(args, format);
|
|
int count = vsnprintf(out + *used, capacity - *used, format, args);
|
|
va_end(args);
|
|
if (count < 0 || (size_t)count >= capacity - *used) return false;
|
|
*used += (size_t)count; return true;
|
|
}
|
|
static bool append_ssid(char *out, size_t capacity, size_t *used, const uint8_t *ssid, size_t length)
|
|
{
|
|
if (length > WIFI_CONFIG_SSID_MAX_LEN || !append(out, capacity, used, "\"")) return false;
|
|
for (size_t i = 0; i < length; ++i) {
|
|
unsigned c = ssid[i];
|
|
if (c >= 0x20 && c <= 0x7e && c != '"' && c != '\\') {
|
|
if (!append(out, capacity, used, "%c", c)) return false;
|
|
} else if (!append(out, capacity, used, "\\u%04x", c)) return false;
|
|
}
|
|
return append(out, capacity, used, "\"");
|
|
}
|
|
static const char *json_bool(bool value) { return value ? "true" : "false"; }
|
|
|
|
static esp_err_t snapshot_response(httpd_req_t *request)
|
|
{
|
|
wifi_manager_settings_t wifi;
|
|
mdns_service_snapshot_t mdns;
|
|
/* No blocking config getters, driver/NVS calls or secret-bearing copies on HTTPD. */
|
|
if (wifi_manager_get_settings(&wifi) != ESP_OK || mdns_service_get_settings(&mdns) != ESP_OK)
|
|
return respond(request, "503 Service Unavailable", "{\"error\":\"snapshot_unavailable\"}");
|
|
char response[WEB_NETWORK_SNAPSHOT_MAX]; size_t used = 0;
|
|
#define ADD(...) do { if (!append(response, sizeof(response), &used, __VA_ARGS__)) return ESP_FAIL; } while (0)
|
|
#define SSID(data, length) do { if (!append_ssid(response, sizeof(response), &used, data, length)) return ESP_FAIL; } while (0)
|
|
ADD("{\"wifi\":{\"generation\":%" PRIu32 ",\"enabled_at_boot\":%s,\"ap\":{\"policy\":\"%s\",\"channel\":%u,\"ssid\":",
|
|
wifi.runtime.config_generation, json_bool(wifi.enabled_at_boot),
|
|
wifi_config_ap_policy_to_string(wifi.ap_policy), (unsigned)wifi.ap_channel);
|
|
SSID(wifi.ap_ssid, wifi.ap_ssid_len);
|
|
ADD(",\"password_configured\":%s},\"profiles\":[", json_bool(wifi.ap_password_configured));
|
|
for (unsigned i = 0; i < WIFI_CONFIG_STA_PROFILE_COUNT; ++i) {
|
|
const wifi_manager_profile_settings_t *p = &wifi.profiles[i];
|
|
ADD("%s{\"index\":%u,\"enabled\":%s,\"priority\":%u,\"security\":\"%s\",\"ssid\":",
|
|
i ? "," : "", i, json_bool(p->enabled), (unsigned)p->priority, wifi_config_security_to_string(p->security));
|
|
SSID(p->ssid, p->ssid_len);
|
|
ADD(",\"password_configured\":%s}", json_bool(p->password_configured));
|
|
}
|
|
const wifi_manager_snapshot_t *r = &wifi.runtime;
|
|
/* IPv4 bytes are already in network order, independent of host endianness. */
|
|
const uint8_t *ip = (const uint8_t *)&r->ip;
|
|
ADD("]},\"runtime\":{\"started\":%s,\"state\":\"%s\",\"active_profile\":%d,\"ip\":\"%u.%u.%u.%u\","
|
|
"\"ap_running\":%s,\"ap_clients\":%u,\"last_error\":%d},",
|
|
json_bool(r->started), wifi_manager_state_to_string(r->state), (int)r->active_profile,
|
|
ip[0], ip[1], ip[2], ip[3], json_bool(r->ap_running), (unsigned)r->ap_client_count, (int)r->last_error);
|
|
ADD("\"mdns\":{\"generation\":%" PRIu32 ",\"suffix\":\"%s\",\"hostname\":\"%s\",\"announced\":%s,\"last_error\":%d}}",
|
|
mdns.config_generation, mdns.suffix, mdns.hostname, json_bool(mdns.announced), (int)mdns.last_error);
|
|
#undef SSID
|
|
#undef ADD
|
|
return respond(request, "200 OK", response);
|
|
}
|
|
|
|
esp_err_t web_network_snapshot_handler(httpd_req_t *request)
|
|
{
|
|
web_session_view_t view = {0}; bool allowed = false;
|
|
esp_err_t error = authorize(request, false, &view, &allowed);
|
|
if (error == ESP_OK && allowed) {
|
|
error = request->method == HTTP_GET ? snapshot_response(request) :
|
|
respond(request, "405 Method Not Allowed", "{\"error\":\"method\"}");
|
|
}
|
|
secure_wipe(&view, sizeof(view));
|
|
web_httpd_wipe_request(request, web_httpd_unread_body(request));
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_network_operation_handler(httpd_req_t *request)
|
|
{
|
|
web_session_view_t view = {0}; bool allowed = false;
|
|
network_operation_t operation = {0};
|
|
bool mutation = request->method == HTTP_POST;
|
|
esp_err_t error = authorize(request, mutation, &view, &allowed);
|
|
if (error != ESP_OK || !allowed) goto done;
|
|
if (request->method != HTTP_GET && !mutation) {
|
|
error = respond(request, "405 Method Not Allowed", "{\"error\":\"method\"}"); goto done;
|
|
}
|
|
if (mutation) {
|
|
char type[40] = {0}, body[WEB_NETWORK_REQUEST_MAX]; size_t received = 0;
|
|
bool valid = request->content_len > 0 && request->content_len <= sizeof(body) &&
|
|
httpd_req_get_hdr_value_str(request, "Content-Type", type, sizeof(type)) == ESP_OK &&
|
|
(!strcmp(type, "application/json") || !strcmp(type, "application/json; charset=utf-8"));
|
|
for (unsigned reads = 0; valid && received < request->content_len && reads < 4; ++reads) {
|
|
int count = httpd_req_recv(request, body + received, request->content_len - received);
|
|
if (count <= 0 || (size_t)count > request->content_len - received) valid = false;
|
|
else received += (size_t)count;
|
|
}
|
|
valid = valid && received == request->content_len && parse_request(body, received, &operation);
|
|
secure_wipe(body, sizeof(body));
|
|
if (!valid) {
|
|
wipe_input(&operation);
|
|
error = respond(request, "400 Bad Request", "{\"error\":\"invalid_network_request\"}"); goto done;
|
|
}
|
|
if (!ensure_secret_timer()) {
|
|
wipe_input(&operation);
|
|
error = respond(request, "503 Service Unavailable", "{\"error\":\"timer_unavailable\"}"); goto done;
|
|
}
|
|
operation.session = view.id; operation.principal = view.principal;
|
|
operation.deadline = esp_timer_get_time() + 30000000LL; operation.state = PENDING;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool busy = s_operation.state == PENDING || s_next_id == UINT32_MAX;
|
|
if (!busy) { operation.id = ++s_next_id; s_operation = operation; }
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
wipe_input(&operation);
|
|
if (busy || admin_ssh_console_submit_network_settings(operation.id) != ESP_OK) {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (!busy && s_operation.id == operation.id) secure_wipe(&s_operation, sizeof(s_operation));
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
error = httpd_resp_set_hdr(request, "Retry-After", "1");
|
|
if (error == ESP_OK) error = respond(request, "503 Service Unavailable", "{\"error\":\"busy\"}");
|
|
goto done;
|
|
}
|
|
} else {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_operation.session == view.id) {
|
|
operation.id = s_operation.id; operation.action = s_operation.action;
|
|
operation.state = s_operation.state; operation.error = s_operation.error;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
/* Input is not needed for formatting or potentially blocking socket IO. */
|
|
wipe_input(&operation);
|
|
char response[128];
|
|
int written = snprintf(response, sizeof(response), "{\"id\":%" PRIu32 ",\"action\":\"%s\",\"state\":\"%s\",\"error\":%d}",
|
|
operation.id, operation.id ? s_actions[operation.action] : "none", s_states[operation.state], (int)operation.error);
|
|
error = written < 0 || (size_t)written >= sizeof(response) ? ESP_FAIL :
|
|
respond(request, mutation ? "202 Accepted" : "200 OK", response);
|
|
done:
|
|
secure_wipe(&operation, sizeof(operation));
|
|
secure_wipe(&view, sizeof(view));
|
|
web_httpd_wipe_request(request, web_httpd_unread_body(request));
|
|
return error;
|
|
}
|