Commit incoming attachment requests before acknowledging them
This commit is contained in:
1 parent
15ec41f2fd
commit
760398b96c
2 files changed
+112
-34
No files matched your search
+108
-31
@@ -341,19 +341,35 @@ done:
|
||||
|
||||
/* Кодограммы привязаны к immutable сообщению, UUID и полному hash контейнера. */
|
||||
static int ak_body(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint8_t cmd, uint8_t wire[AK_WIRE]) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "SELECT crypto_id,cipher_hash,key FROM attachment_keys WHERE channel=? AND message_id=?");
|
||||
if (!st) return -1;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
int rc = sqlite3_step(st), ok = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 16 && sqlite3_column_bytes(st, 1) == 32 &&
|
||||
(cmd != AK_GRANT || sqlite3_column_bytes(st, 2) == 32);
|
||||
if (ok) {
|
||||
memcpy(wire, "AK01", 4); wire[4] = cmd; memcpy(wire + 5, message, 32);
|
||||
memcpy(wire + 37, sqlite3_column_blob(st, 0), 16); memcpy(wire + 53, sqlite3_column_blob(st, 1), 32);
|
||||
if (cmd == AK_GRANT) memcpy(wire + AK_BODY, sqlite3_column_blob(st, 2), 32);
|
||||
sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL;
|
||||
const char* stage = "prepare";
|
||||
int rc = sqlite3_prepare_v2(db, "SELECT crypto_id,cipher_hash,key FROM attachment_keys WHERE channel=? AND message_id=?", -1, &st, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind message"; rc = sqlite3_bind_blob(st, 2, message, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "step"; rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_ROW) goto fail;
|
||||
if (sqlite3_column_bytes(st, 0) != 16 || sqlite3_column_bytes(st, 1) != 32 ||
|
||||
(cmd == AK_GRANT && sqlite3_column_bytes(st, 2) != 32)) {
|
||||
stage = "identity/key size"; rc = SQLITE_MISMATCH; goto fail;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (!ok) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: wire identity/key unavailable channel=%s command=%u rc=%d", channel, cmd, rc);
|
||||
return ok ? (cmd == AK_GRANT ? AK_WIRE : AK_BODY) : -1;
|
||||
const void* id = sqlite3_column_blob(st, 0); const void* hash = sqlite3_column_blob(st, 1);
|
||||
const void* key = cmd == AK_GRANT ? sqlite3_column_blob(st, 2) : NULL;
|
||||
if (!id || !hash || (cmd == AK_GRANT && !key)) { stage = "identity/key allocation"; rc = SQLITE_NOMEM; goto fail; }
|
||||
memcpy(wire, "AK01", 4); wire[4] = cmd; memcpy(wire + 5, message, 32);
|
||||
memcpy(wire + 37, id, 16); memcpy(wire + 53, hash, 32);
|
||||
if (cmd == AK_GRANT) memcpy(wire + AK_BODY, key, 32);
|
||||
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
return cmd == AK_GRANT ? AK_WIRE : AK_BODY;
|
||||
fail:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: wire identity/key channel=%s command=%u stage=%s rc=%d (%s) db=%s",
|
||||
channel, cmd, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
|
||||
int final_rc = sqlite3_finalize(st);
|
||||
if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: wire cleanup finalize rc=%d", final_rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Неподписанные и незашифрованные ответы сервис не принимает. */
|
||||
@@ -440,6 +456,82 @@ static void ak_ack(struct UTUN_INSTANCE* inst, const char* channel, uint64_t pee
|
||||
if (rc < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: acknowledgment deferred channel=%s command=%u", channel, cmd);
|
||||
}
|
||||
|
||||
/* Новый запрос и повтор прежнего решения фиксируются до ACK; внешняя транзакция остаётся вызывающему. */
|
||||
static int ak_accept_request(struct UTUN_INSTANCE* inst, const char* channel, uint64_t peer, const uint8_t* body) {
|
||||
sqlite3* db = CC(inst)->db;
|
||||
if (!sqlite3_get_autocommit(db)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: request rejected inside external transaction channel=%s peer=%llu",
|
||||
channel, (unsigned long long)peer); return -1;
|
||||
}
|
||||
sqlite3_stmt* st = NULL; uint8_t expected[AK_WIRE];
|
||||
const char* stage = "begin";
|
||||
int rc = sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "identity";
|
||||
if (ak_body(inst, channel, body + 5, AK_REQUEST, expected) < 0 || memcmp(body + 5, expected + 5, AK_BODY - 5)) {
|
||||
rc = SQLITE_MISMATCH; goto fail;
|
||||
}
|
||||
stage = "authorization";
|
||||
if (!ak_authorized(inst, channel, body + 5, peer)) { rc = SQLITE_AUTH; goto fail; }
|
||||
stage = "read prepare";
|
||||
rc = sqlite3_prepare_v2(db, "SELECT incoming,state FROM attachment_key_requests WHERE channel=? AND message_id=? AND peer=?", -1, &st, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "read bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "read bind message"; rc = sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "read bind peer"; rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "read step"; rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_ROW && rc != SQLITE_DONE) goto fail;
|
||||
int exists = rc == SQLITE_ROW, incoming = exists ? sqlite3_column_int(st, 0) : 1;
|
||||
int state = exists ? sqlite3_column_int(st, 1) : 0;
|
||||
stage = "read finalize"; rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
if (incoming != 1 || (state != 0 && state != 2 && state != 3 && state != 4)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: request conflict channel=%s peer=%llu incoming=%d state=%d",
|
||||
channel, (unsigned long long)peer, incoming, state);
|
||||
stage = "request state"; rc = SQLITE_CONSTRAINT; goto fail;
|
||||
}
|
||||
if (!exists) {
|
||||
stage = "insert prepare";
|
||||
rc = sqlite3_prepare_v2(db, "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at)"
|
||||
" VALUES(?,?,?,1,0,unixepoch())", -1, &st, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "insert bind channel"; rc = sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "insert bind message"; rc = sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "insert bind peer"; rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "insert step"; rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_DONE) goto fail;
|
||||
int changed = sqlite3_changes(db);
|
||||
stage = "insert finalize"; rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
if (changed != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: request insert rows=%d expected=1 channel=%s peer=%llu",
|
||||
changed, channel, (unsigned long long)peer);
|
||||
stage = "insert row count"; rc = SQLITE_CONSTRAINT; goto fail;
|
||||
}
|
||||
}
|
||||
int command = state == 0 ? 0 : state == 3 ? AK_DENY : AK_GRANT;
|
||||
stage = "outbox";
|
||||
if (command && ak_queue(inst, channel, body + 5, peer, command)) { rc = SQLITE_ERROR; goto fail; }
|
||||
stage = "commit"; rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: request committed channel=%s peer=%llu message=%02x%02x%02x%02x duplicate=%d state=%d queued=%d",
|
||||
channel, (unsigned long long)peer, body[5], body[6], body[7], body[8], exists, state, command);
|
||||
return 0;
|
||||
fail:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: request channel=%s peer=%llu stage=%s rc=%d (%s) db=%s",
|
||||
channel, (unsigned long long)peer, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
|
||||
int final_rc = sqlite3_finalize(st);
|
||||
if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: request cleanup finalize rc=%d", final_rc);
|
||||
ak_rollback(inst, "request", channel);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Ответ меняет ключ, запрос и очередь одним commit; сетевое подтверждение остаётся вызывающему. */
|
||||
static int ak_apply_result(struct UTUN_INSTANCE* inst, const char* channel, uint64_t peer, const uint8_t* body, int* next_out) {
|
||||
sqlite3* db = CC(inst)->db;
|
||||
@@ -569,33 +661,18 @@ static void ak_receive(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
char channel[32];
|
||||
if (!CC(inst) || CC(inst)->attachment_closing || entry->len < ROUTER_SVC_PAYLOAD_OFF + AK_BODY) goto invalid;
|
||||
memcpy(&peer, d + ROUTER_SVC_SRC_OFF, 8); memcpy(&group, d + ROUTER_SVC_GROUP_OFF, 8); memcpy(&target, d + ROUTER_SVC_DST_OFF, 8);
|
||||
if (target != inst->node_id || peer == inst->node_id ||
|
||||
if (target != inst->node_id || !peer || peer == inst->node_id ||
|
||||
(d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) goto invalid;
|
||||
snprintf(channel, sizeof(channel), "%llu", (unsigned long long)group);
|
||||
uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF; size_t bytes = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
uint8_t cmd = body[4], expected[AK_WIRE];
|
||||
if (!ak_channel(channel) || memcmp(body, "AK01", 4) || cmd < AK_REQUEST || cmd > AK_RESULT_ACK ||
|
||||
bytes != (cmd == AK_GRANT ? AK_WIRE : AK_BODY) || ak_body(inst, channel, body + 5, AK_REQUEST, expected) < 0 ||
|
||||
memcmp(body + 5, expected + 5, AK_BODY - 5)) goto invalid;
|
||||
bytes != (cmd == AK_GRANT ? AK_WIRE : AK_BODY)) goto invalid;
|
||||
if (cmd == AK_REQUEST) {
|
||||
if (!ak_authorized(inst, channel, body + 5, peer) || ak_exec(inst, "SAVEPOINT attachment_request")) goto invalid;
|
||||
sqlite3_stmt* st = ak_prepare(inst, "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at)"
|
||||
" VALUES(?,?,?,1,0,unixepoch()) ON CONFLICT(channel,message_id,peer) DO NOTHING");
|
||||
if (st) { sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer); }
|
||||
if (!st || ak_step(inst, st)) goto rollback_request;
|
||||
st = ak_prepare(inst, "SELECT state FROM attachment_key_requests WHERE channel=? AND message_id=? AND peer=? AND incoming=1");
|
||||
if (!st) goto rollback_request;
|
||||
sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 2, body + 5, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)peer);
|
||||
int rc = sqlite3_step(st), state = rc == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st);
|
||||
if (state < 0 || (state && ak_queue(inst, channel, body + 5, peer, state == 3 ? AK_DENY : AK_GRANT)) ||
|
||||
ak_exec(inst, "RELEASE attachment_request")) goto rollback_request;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: request saved channel=%s peer=%llu state=%d", channel, (unsigned long long)peer, state);
|
||||
if (ak_accept_request(inst, channel, peer, body)) goto done;
|
||||
ak_ack(inst, channel, peer, body, AK_REQUEST_ACK); ak_changed(inst, channel); goto done;
|
||||
rollback_request:
|
||||
ak_exec(inst, "ROLLBACK TO attachment_request"); ak_exec(inst, "RELEASE attachment_request"); goto done;
|
||||
}
|
||||
if (ak_body(inst, channel, body + 5, AK_REQUEST, expected) < 0 || memcmp(body + 5, expected + 5, AK_BODY - 5)) goto invalid;
|
||||
int next;
|
||||
if (ak_apply_result(inst, channel, peer, body, &next)) goto done;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: control committed channel=%s peer=%llu command=%u state=%d", channel,
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* Ответы REQUEST_ACK/GRANT/DENY/RESULT_ACK атомарно меняют ключ, запрос и outbox собственной транзакцией.
|
||||
* Внешняя транзакция для ответов запрещена. GRANT/DENY подтверждаются только после проверенного commit;
|
||||
* повтор с тем же результатом допустим, конфликтующий ключ/решение отклоняется. Отказ SQL сохраняет прежнее состояние.
|
||||
* Незавершённый rollback отключает модуль и запрашивает остановку ядра владельцем вне callback.
|
||||
* Известное ограничение приёма REQUEST: вложенный SAVEPOINT допускает ACK/событие до commit внешней транзакции;
|
||||
* её rollback теряет уже подтверждённый запрос. См. doc/chat_audit_findings.txt, «Аудит приёма REQUEST». */
|
||||
* Приём REQUEST владеет собственной транзакцией проверки идентичности/прав и сохранения запроса/повтора ответа.
|
||||
* Внешняя транзакция отклоняется без изменений; ACK/событие — после commit. Дубликат сохраняет решение и дату:
|
||||
* state=0 ждёт автора, 2/4 повторяют GRANT, 3 повторяет DENY; чужое направление и другие состояния отклоняются.
|
||||
* Незавершённый rollback отключает модуль и запрашивает остановку ядра владельцем вне callback. */
|
||||
#ifndef CHAT_ATTACHMENT_H
|
||||
#define CHAT_ATTACHMENT_H
|
||||
#include "../media_async/attachment.h"
|
||||
|
||||
Reference in new issue
Block a user