485 lines
23 KiB
C
485 lines
23 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
#include "web_account_settings.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "admin_ssh_console.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "secure_random.h"
|
|
#include "mbedtls/base64.h"
|
|
#include "ssh_transport.h"
|
|
#include "web_cookie_auth.h"
|
|
#include "web_auth_parse.h"
|
|
#include "web_httpd_adapter.h"
|
|
#include "web_serial_transport.h"
|
|
|
|
enum { IDLE, PENDING, OK, FAILED, CANCELLED, STALE, PROTECTED, DUPLICATE, FULL };
|
|
static const char *const s_states[] = {"idle", "pending", "ok", "failed", "cancelled", "stale", "protected", "duplicate", "full"};
|
|
typedef enum { ACTION_ROLE, ACTION_DELETE, ACTION_CREATE, ACTION_PASSWORD,
|
|
ACTION_KEY_ADD, ACTION_KEY_DELETE, ACTION_KEY_CLEAR } account_action_t;
|
|
static const char *const s_actions[] = {"role", "delete", "create", "password", "key-add", "key-delete", "key-clear"};
|
|
typedef struct {
|
|
uint32_t id;
|
|
web_session_id_t session;
|
|
user_principal_t principal;
|
|
user_database_account_t target;
|
|
int64_t deadline;
|
|
user_role_t role;
|
|
unsigned state;
|
|
account_action_t action;
|
|
bool executing;
|
|
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U];
|
|
size_t password_length;
|
|
char key_type[USER_DATABASE_SSH_KEY_TYPE_CAPACITY + 1U];
|
|
uint8_t key_blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY];
|
|
size_t key_blob_length;
|
|
uint8_t key_index;
|
|
} account_operation_t;
|
|
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
|
static account_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 bool credential_action(account_action_t action)
|
|
{
|
|
return action == ACTION_CREATE || action == ACTION_PASSWORD;
|
|
}
|
|
|
|
static void wipe_input(account_operation_t *operation)
|
|
{
|
|
secure_wipe(&operation->principal, sizeof(operation->principal));
|
|
secure_wipe(&operation->target, sizeof(operation->target));
|
|
secure_wipe(operation->password, sizeof(operation->password));
|
|
operation->password_length = 0;
|
|
secure_wipe(operation->key_type, sizeof(operation->key_type));
|
|
secure_wipe(operation->key_blob, sizeof(operation->key_blob));
|
|
operation->key_blob_length = 0;
|
|
operation->key_index = 0;
|
|
}
|
|
|
|
static void expire_secret(void *unused)
|
|
{
|
|
(void)unused;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
/* Inspect only the current ID/deadline, never a captured/rearmed job. A late
|
|
* tick cannot cancel a replacement before its own deadline or executing work. */
|
|
if (s_operation.id && s_operation.state == PENDING && !s_operation.executing &&
|
|
credential_action(s_operation.action) && esp_timer_get_time() >= s_operation.deadline) {
|
|
s_operation.state = CANCELLED;
|
|
wipe_input(&s_operation);
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
|
|
static bool ensure_secret_timer(void)
|
|
{
|
|
/* HTTPD is the sole admission owner. Once started, this one firmware-lifetime
|
|
* timer is never stopped/rearmed/deleted. Expiry is best-effort scheduling,
|
|
* not hard realtime; no network/database work runs in its callback. */
|
|
if (!s_secret_timer) {
|
|
const esp_timer_create_args_t args = {.callback = expire_secret, .name = "account-secret"};
|
|
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;
|
|
}
|
|
|
|
/* OpenSSH text envelope only. The canonical database parser validates the SSH
|
|
* blob (including the P256 point) on the dispatcher, not the HTTPD stack. */
|
|
static bool parse_public_key(const char *text, size_t length, account_operation_t *operation)
|
|
{
|
|
size_t type_length = 0;
|
|
while (type_length < length && text[type_length] != ' ' && text[type_length] != '\t') ++type_length;
|
|
if (!((type_length == 11 && !memcmp(text, "ssh-ed25519", 11)) ||
|
|
(type_length == 19 && !memcmp(text, "ecdsa-sha2-nistp256", 19)))) return false;
|
|
size_t start = type_length;
|
|
while (start < length && (text[start] == ' ' || text[start] == '\t')) ++start;
|
|
size_t end = start;
|
|
while (end < length && text[end] != ' ' && text[end] != '\t') ++end;
|
|
size_t encoded_length = end - start;
|
|
if (!encoded_length || encoded_length > 172 || encoded_length % 4) return false;
|
|
for (size_t i = start; i < end; ++i) {
|
|
unsigned char c = (unsigned char)text[i];
|
|
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '+' || c == '/' ||
|
|
(c == '=' && i >= end - 2))) return false;
|
|
}
|
|
for (size_t i = end; i < length; ++i)
|
|
if ((text[i] < ' ' || text[i] > '~') && text[i] != '\t') return false;
|
|
if (mbedtls_base64_decode(operation->key_blob, sizeof(operation->key_blob),
|
|
&operation->key_blob_length, (const uint8_t *)text + start, encoded_length) != 0) return false;
|
|
/* Round-trip rejects noncanonical padding and unused base64 bits. */
|
|
unsigned char encoded[173];
|
|
size_t written = 0;
|
|
if (mbedtls_base64_encode(encoded, sizeof(encoded), &written, operation->key_blob,
|
|
operation->key_blob_length) != 0 || written != encoded_length ||
|
|
memcmp(encoded, text + start, written)) return false;
|
|
memcpy(operation->key_type, text, type_length);
|
|
return true;
|
|
}
|
|
|
|
/* Exact flat schemas. Password/public_key accept JSON escapes; canonical database
|
|
* policy validates the decoded bytes. No coercion/unknown/duplicate fields. */
|
|
static bool parse_request(const char *body, size_t length, account_operation_t *operation, bool keys_only)
|
|
{
|
|
const char *keys[] = {"action", "username", "user_id", "auth_generation", "role", "password", "public_key", "key_index"};
|
|
unsigned seen = 0;
|
|
size_t pos = 0;
|
|
#define SPACE() while (pos < length && (body[pos] == ' ' || body[pos] == '\t' || body[pos] == '\r' || body[pos] == '\n')) ++pos
|
|
#define TAKE(c) do { SPACE(); if (pos == length || body[pos++] != (c)) return false; } while (0)
|
|
TAKE('{');
|
|
for (unsigned field = 0; field < 8; ++field) {
|
|
if (field) { TAKE(','); }
|
|
TAKE('"');
|
|
size_t start = pos;
|
|
while (pos < length && body[pos] != '"') ++pos;
|
|
if (pos == length) return false;
|
|
unsigned key = 0;
|
|
for (; key < 8; ++key)
|
|
if (strlen(keys[key]) == pos - start && !memcmp(body + start, keys[key], pos - start)) break;
|
|
if (key == 8 || (seen & (1U << key))) return false;
|
|
++pos; TAKE(':'); SPACE();
|
|
uint32_t number = 0;
|
|
char value[USER_DATABASE_USERNAME_CAPACITY + 1] = {0};
|
|
if (key == 2 || key == 3 || key == 7) {
|
|
start = pos;
|
|
while (pos < length && body[pos] >= '0' && body[pos] <= '9') {
|
|
unsigned digit = (unsigned)(body[pos++] - '0');
|
|
if (number > (UINT32_MAX - digit) / 10U) return false;
|
|
number = number * 10U + digit;
|
|
}
|
|
if ((!number && key != 7) || pos == start || (pos - start > 1 && body[start] == '0')) return false;
|
|
if (key == 7 && number >= USER_DATABASE_MAX_SSH_KEYS_PER_USER) return false;
|
|
} else if (key == 6) {
|
|
char text[385] = {0};
|
|
size_t text_length = 0;
|
|
if (!web_auth_parse_json_string(body, length, &pos, (uint8_t *)text,
|
|
sizeof(text), &text_length) || !parse_public_key(text, text_length, operation)) return false;
|
|
} else if (key == 5) {
|
|
if (!web_auth_parse_json_string(body, length, &pos, operation->password,
|
|
sizeof(operation->password), &operation->password_length) ||
|
|
!user_database_password_valid(operation->password, operation->password_length)) return false;
|
|
} else {
|
|
TAKE('"'); start = pos;
|
|
while (pos < length && body[pos] != '"') {
|
|
if (body[pos] < ' ' || body[pos] > '~' || body[pos] == '\\' || pos - start >= sizeof(value) - 1) return false;
|
|
++pos;
|
|
}
|
|
if (pos == length) return false;
|
|
memcpy(value, body + start, pos - start); ++pos;
|
|
}
|
|
switch (key) {
|
|
case 0:
|
|
{
|
|
unsigned action = 0;
|
|
for (; action < sizeof(s_actions) / sizeof(*s_actions); ++action)
|
|
if (!strcmp(value, s_actions[action])) break;
|
|
if (action == sizeof(s_actions) / sizeof(*s_actions)) return false;
|
|
operation->action = (account_action_t)action;
|
|
}
|
|
break;
|
|
case 1:
|
|
if (!user_database_username_valid((const uint8_t *)value, strlen(value))) return false;
|
|
memcpy(operation->target.username, value, sizeof(value)); break;
|
|
case 2: operation->target.user_id = number; break;
|
|
case 3: operation->target.auth_generation = number; break;
|
|
case 4: if (!user_role_parse(value, &operation->role)) return false; break;
|
|
case 7: operation->key_index = (uint8_t)number; break;
|
|
}
|
|
seen |= 1U << key;
|
|
SPACE();
|
|
if (pos < length && body[pos] == '}') break;
|
|
}
|
|
TAKE('}'); SPACE();
|
|
#undef TAKE
|
|
#undef SPACE
|
|
const unsigned schemas[] = {31U, 15U, 51U, 47U, 79U, 143U, 15U};
|
|
return pos == length && seen == (keys_only ? 14U : schemas[operation->action]);
|
|
}
|
|
|
|
static bool parse(const char *body, size_t length, account_operation_t *operation)
|
|
{
|
|
return parse_request(body, length, operation, false);
|
|
}
|
|
|
|
void web_account_settings_execute(uint32_t id)
|
|
{
|
|
account_operation_t operation = {0};
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool admitted = id && s_operation.id == id && s_operation.state == PENDING && !s_operation.executing;
|
|
if (admitted) {
|
|
s_operation.executing = true;
|
|
operation = s_operation;
|
|
wipe_input(&s_operation);
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!admitted) 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) {
|
|
/* CLI and typed mutations share this dispatcher. Target identity is also
|
|
* compared under the database mutation lock, not just at HTTP admission. */
|
|
switch (operation.action) {
|
|
case ACTION_ROLE: error = user_database_set_role_current(&operation.target, operation.role); break;
|
|
case ACTION_DELETE: error = user_database_delete_current(&operation.target); break;
|
|
case ACTION_CREATE:
|
|
error = user_database_create((const uint8_t *)operation.target.username,
|
|
strlen(operation.target.username), operation.role, operation.password, operation.password_length);
|
|
break;
|
|
case ACTION_KEY_ADD:
|
|
error = user_database_add_ssh_key_current(&operation.target,
|
|
(const uint8_t *)operation.key_type, strlen(operation.key_type),
|
|
operation.key_blob, operation.key_blob_length, &operation.key_index);
|
|
break;
|
|
case ACTION_KEY_DELETE:
|
|
error = user_database_remove_ssh_key_current(&operation.target, operation.key_index);
|
|
break;
|
|
case ACTION_KEY_CLEAR:
|
|
error = user_database_clear_ssh_keys_current(&operation.target);
|
|
break;
|
|
case ACTION_PASSWORD:
|
|
error = user_database_set_password_current(&operation.target, operation.password, operation.password_length);
|
|
break;
|
|
}
|
|
secure_wipe(operation.password, sizeof(operation.password));
|
|
operation.password_length = 0;
|
|
state = error == ESP_OK ? OK : error == ESP_ERR_NOT_FOUND ? STALE :
|
|
error == ESP_ERR_INVALID_STATE ? (operation.action == ACTION_CREATE ? DUPLICATE :
|
|
operation.action <= ACTION_PASSWORD ? PROTECTED : FAILED) :
|
|
error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY && operation.action == ACTION_KEY_ADD ? DUPLICATE :
|
|
error == ESP_ERR_NO_MEM && (operation.action == ACTION_CREATE || operation.action == ACTION_KEY_ADD) ? FULL : FAILED;
|
|
if (error == ESP_OK && operation.action != ACTION_CREATE) {
|
|
size_t length = strlen(operation.target.username);
|
|
(void)web_serial_transport_revoke_user((const uint8_t *)operation.target.username, length);
|
|
(void)ssh_transport_revoke_user((const uint8_t *)operation.target.username, length);
|
|
}
|
|
}
|
|
secure_wipe(operation.password, sizeof(operation.password));
|
|
operation.password_length = 0;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_operation.id == id && s_operation.state == PENDING && s_operation.executing) {
|
|
s_operation.state = state;
|
|
s_operation.executing = false;
|
|
wipe_input(&s_operation);
|
|
}
|
|
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 list_accounts(httpd_req_t *request)
|
|
{
|
|
user_database_accounts_t accounts;
|
|
if (user_database_get_accounts(&accounts) != ESP_OK)
|
|
return respond(request, "503 Service Unavailable", "{\"error\":\"accounts_unavailable\"}");
|
|
char body[1024];
|
|
size_t used = (size_t)snprintf(body, sizeof(body), "{\"users\":[");
|
|
for (size_t i = 0; i < accounts.count; ++i) {
|
|
const user_database_account_t *user = &accounts.users[i];
|
|
/* Database username policy makes these ASCII strings JSON-safe. */
|
|
int written = snprintf(body + used, sizeof(body) - used,
|
|
"%s{\"username\":\"%s\",\"user_id\":%" PRIu32 ",\"auth_generation\":%" PRIu32 ",\"role\":\"%s\"}",
|
|
i ? "," : "", user->username, user->user_id, user->auth_generation, user_role_to_string(user->role));
|
|
if (written < 0 || (size_t)written >= sizeof(body) - used) return ESP_FAIL;
|
|
used += (size_t)written;
|
|
}
|
|
if (used + 3 > sizeof(body)) return ESP_FAIL;
|
|
memcpy(body + used, "]}", 3);
|
|
return respond(request, "200 OK", body);
|
|
}
|
|
|
|
static bool read_request(httpd_req_t *request, account_operation_t *operation, bool keys_only)
|
|
{
|
|
char type[40] = {0}, body[768];
|
|
size_t received = 0;
|
|
bool valid = request->content_len && 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 &&
|
|
(keys_only ? parse_request(body, received, operation, true) : parse(body, received, operation));
|
|
secure_wipe(body, sizeof(body));
|
|
return valid;
|
|
}
|
|
|
|
esp_err_t web_account_keys_handler(httpd_req_t *request)
|
|
{
|
|
web_session_view_t view = {0};
|
|
account_operation_t operation = {0};
|
|
user_database_user_snapshot_t snapshot = {0};
|
|
bool allowed = false;
|
|
esp_err_t error = web_cookie_auth_require_json(request, 768, &view, &allowed);
|
|
if (error != ESP_OK || !allowed) goto done;
|
|
if (view.principal.role != USER_ROLE_ADMIN) {
|
|
error = respond(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
|
|
goto done;
|
|
}
|
|
if (strcmp(request->uri, "/api/settings/accounts/keys") || !read_request(request, &operation, true)) {
|
|
error = respond(request, "400 Bad Request", "{\"error\":\"invalid_account_request\"}");
|
|
goto done;
|
|
}
|
|
error = user_database_get_account_keys(&operation.target, &snapshot);
|
|
if (error != ESP_OK) {
|
|
error = error == ESP_ERR_NOT_FOUND ? respond(request, "409 Conflict", "{\"error\":\"stale\"}") :
|
|
respond(request, "503 Service Unavailable", "{\"error\":\"accounts_unavailable\"}");
|
|
goto done;
|
|
}
|
|
char body[512];
|
|
int written = snprintf(body, sizeof(body),
|
|
"{\"username\":\"%s\",\"user_id\":%" PRIu32 ",\"auth_generation\":%" PRIu32 ",\"keys\":[",
|
|
snapshot.username, snapshot.user_id, snapshot.auth_generation);
|
|
if (written < 0 || (size_t)written >= sizeof(body)) { error = ESP_FAIL; goto done; }
|
|
size_t used = (size_t)written;
|
|
bool comma = false;
|
|
for (size_t i = 0; i < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++i) {
|
|
const user_database_key_snapshot_t *key = &snapshot.public_keys[i];
|
|
if (!key->active) continue;
|
|
unsigned char fingerprint[45];
|
|
size_t length = 0;
|
|
if (mbedtls_base64_encode(fingerprint, sizeof(fingerprint), &length,
|
|
key->sha256_fingerprint, sizeof(key->sha256_fingerprint)) != 0 || length != 44) {
|
|
error = ESP_FAIL; goto done;
|
|
}
|
|
fingerprint[43] = 0; /* OpenSSH SHA256 fingerprints omit base64 padding. */
|
|
written = snprintf(body + used, sizeof(body) - used,
|
|
"%s{\"index\":%u,\"type\":\"%s\",\"fingerprint\":\"SHA256:%s\"}",
|
|
comma ? "," : "", key->index, key->key_type, (const char *)fingerprint);
|
|
if (written < 0 || (size_t)written >= sizeof(body) - used) { error = ESP_FAIL; goto done; }
|
|
used += (size_t)written;
|
|
comma = true;
|
|
}
|
|
if (used + 3 > sizeof(body)) { error = ESP_FAIL; goto done; }
|
|
memcpy(body + used, "]}", 3);
|
|
error = respond(request, "200 OK", body);
|
|
done:
|
|
secure_wipe(&operation, sizeof(operation));
|
|
secure_wipe(&view, sizeof(view));
|
|
web_httpd_wipe_request(request, web_httpd_unread_body(request));
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_account_generate_password_handler(httpd_req_t *request)
|
|
{
|
|
web_session_view_t view = {0};
|
|
user_database_generated_password_t generated = {0};
|
|
char response[96] = {0};
|
|
bool allowed = false;
|
|
esp_err_t error = web_cookie_auth_require(request, true, false, &view, &allowed);
|
|
if (error != ESP_OK || !allowed) goto done;
|
|
if (view.principal.role != USER_ROLE_ADMIN) {
|
|
error = respond(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
|
|
goto done;
|
|
}
|
|
error = user_database_generate_password_value(&generated);
|
|
if (error != ESP_OK) {
|
|
secure_wipe(&generated, sizeof(generated));
|
|
error = respond(request, "503 Service Unavailable", "{\"error\":\"unavailable\"}");
|
|
goto done;
|
|
}
|
|
bool current = false;
|
|
error = web_session_store_check_principal(view.id, &view.principal, ¤t);
|
|
if (error != ESP_OK || !current) {
|
|
secure_wipe(&generated, sizeof(generated));
|
|
error = respond(request, "401 Unauthorized", "{\"error\":\"authentication_required\"}");
|
|
goto done;
|
|
}
|
|
int written = snprintf(response, sizeof(response), "{\"password\":\"%s\"}", (const char *)generated.password);
|
|
secure_wipe(&generated, sizeof(generated));
|
|
error = written < 0 || (size_t)written >= sizeof(response) ? ESP_FAIL : respond(request, "200 OK", response);
|
|
done:
|
|
secure_wipe(&generated, sizeof(generated));
|
|
secure_wipe(response, sizeof(response));
|
|
secure_wipe(&view, sizeof(view));
|
|
web_httpd_wipe_request(request, web_httpd_unread_body(request));
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_account_settings_handler(httpd_req_t *request)
|
|
{
|
|
web_session_view_t view = {0};
|
|
account_operation_t operation = {0};
|
|
bool allowed = false, mutation = request->method == HTTP_POST;
|
|
esp_err_t error = mutation ? web_cookie_auth_require_json(request, 768, &view, &allowed) :
|
|
web_cookie_auth_require(request, false, false, &view, &allowed);
|
|
if (error != ESP_OK || !allowed) goto done;
|
|
if (view.principal.role != USER_ROLE_ADMIN) {
|
|
error = respond(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
|
|
goto done;
|
|
}
|
|
if (!strcmp(request->uri, "/api/settings/accounts")) {
|
|
error = mutation ? respond(request, "400 Bad Request", "{\"error\":\"invalid_request\"}") : list_accounts(request);
|
|
goto done;
|
|
}
|
|
if (mutation) {
|
|
if (!read_request(request, &operation, false)) {
|
|
wipe_input(&operation);
|
|
error = respond(request, "400 Bad Request", "{\"error\":\"invalid_account_request\"}");
|
|
goto done;
|
|
}
|
|
if (credential_action(operation.action) && !ensure_secret_timer()) {
|
|
wipe_input(&operation);
|
|
error = respond(request, "503 Service Unavailable", "{\"error\":\"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);
|
|
if (busy || admin_ssh_console_submit_account_settings(operation.id) != ESP_OK) {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (!busy && s_operation.id == operation.id && !s_operation.executing)
|
|
secure_wipe(&s_operation, sizeof(s_operation));
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
wipe_input(&operation);
|
|
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;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
wipe_input(&operation);
|
|
char response[96];
|
|
int written = snprintf(response, sizeof(response), "{\"id\":%" PRIu32 ",\"action\":\"%s\",\"state\":\"%s\"}",
|
|
operation.id, operation.id ? s_actions[operation.action] : "none", s_states[operation.state]);
|
|
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;
|
|
}
|