/* Три настоящих ядра A/B/M: подписанный storage, wire-запросы, загрузка A, * скачивание B, запрет чужой записи, удалённого участника и restart сервиса. */ #include "../src/member_files/member_files.h" #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_event.h" #include "../src/chat/member_sync.h" #include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/etcp_router.h" #include "../src/transport_layer/etcp.h" #include "../src/transport_layer/secure_channel.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "test_utils.h" #include #include #include #define GROUP 5858585858585858ULL #define CHANNEL "5858585858585858" #define BYTES (3 * MF_BLOCK_SIZE + 37) enum { AUTHOR, READER, SERVER, NODE_COUNT }; struct network_test { struct UASYNC* ua; struct UTUN_INSTANCE* nodes[NODE_COUNT]; struct SC_MYKEYS keys[NODE_COUNT]; uint64_t ids[NODE_COUNT]; uint8_t ed[NODE_COUNT][32], channel_x[32], channel_ed[32], channel_private[32]; char dir[512], source[768], destination[768]; struct mf_message response; unsigned phase, ticks, progress; int answered, finished, transfer_status, failed; void* timer; }; static struct network_test* active; static void fail(struct network_test* t, const char* why) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "member_files network: FAIL phase=%u reason=%s", t->phase, why); t->failed = 1; uasync_stop(t->ua); } static void hex(const uint8_t* input, char* out) { for (unsigned i = 0; i < 32; i++) snprintf(out + 2 * i, 3, "%02x", input[i]); } static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { (void)inst; struct network_test* t = active; if (type != CHAT_EVT_MEMBER_FILES_TRANSFER || len != 51) return; t->progress++; if (data[50] == 2) { t->finished = 1; t->transfer_status = data[48] | ((int)data[49] << 8); } } static void response(void* arg, const struct mf_message* r) { struct network_test* t = arg; t->response = *r; t->answered = 1; } static int put_member(struct network_test* t, int destination, int member, int deleted) { uint8_t msg[512], join[64], tags_sig[64], tree[64]; uint64_t ts = 100000; int n = member_sync_build_join_msg(t->channel_x, t->channel_ed, t->ids[member], t->keys[member].public_key, ts, msg, sizeof(msg)); if (n < 0 || sc_ed25519_sign(t->nodes[member]->my_ed25519_privkey, msg, n, join) != SC_OK) return -1; const char* tags = deleted ? "{\"deleted\":\"yes\",\"ver\":\"2\"}" : member == SERVER ? "{\"storage\":\"yes\",\"ver\":\"1\"}" : "{\"ver\":\"1\"}"; n = member_sync_build_owner_msg(t->ids[member], tags, msg, sizeof(msg)); if (n < 0 || sc_ed25519_sign(t->channel_private, msg, n, tags_sig) != SC_OK || member_sync_sign_pubkey(t->channel_private, t->keys[member].public_key, tree)) return -1; char name[64]; snprintf(name, sizeof(name), "{\"name\":\"node%d\"}", member); return member_sync_put(t->nodes[destination], CHANNEL, t->ids[member], t->keys[member].public_key, t->ed[member], join, ts, NULL, 0, name, tags, tags_sig, member == SERVER, 0, tree) < 0 ? -1 : 0; } static int start(struct network_test* t, int index, int port) { char pub[65], priv[65], server[65], path[768], db[768]; hex(t->keys[index].public_key, pub); hex(t->keys[index].private_key, priv); hex(t->keys[SERVER].public_key, server); snprintf(path, sizeof(path), "%s/node%d.conf", t->dir, index); snprintf(db, sizeof(db), "%s/db%d", t->dir, index); FILE* f = fopen(path, "w"); if (!f) return -1; fprintf(f, "[global]\nmy_private_key=%s\nmy_public_key=%s\ndb_path=%s\n" "[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n" "[chatserver]\nstorage_autoload=0\nmember_files_quota_mb=8\n", priv, pub, db, port + index); if (index != SERVER) fprintf(f, "[client:to_m]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", server, port + SERVER); fclose(f); t->nodes[index] = utun_instance_create(t->ua, path); if (!t->nodes[index] || utun_instance_init(t->nodes[index])) return -1; memcpy(t->ed[index], t->nodes[index]->my_ed25519_pubkey, 32); t->ids[index] = t->nodes[index]->node_id; chat_event_set_handler(t->nodes[index], event); return 0; } static int setup(struct network_test* t) { struct SC_MYKEYS keys; sc_generate_keypair(&keys); memcpy(t->channel_x, keys.public_key, 32); uint8_t seed[SHA512_DIGEST_LENGTH]; SHA512(keys.private_key, 32, seed); memcpy(t->channel_private, seed, 32); if (sc_derive_ed25519_pubkey(keys.private_key, t->channel_ed)) return -1; uint8_t signature[64], message[256]; size_t at = sizeof(CHANNEL); memcpy(message, CHANNEL, at); const char name[] = "member-files"; memcpy(message + at, name, sizeof(name)); at += sizeof(name); memcpy(message + at, &t->ids[AUTHOR], 8); at += 8; memcpy(message + at, t->channel_x, 32); at += 32; memcpy(message + at, t->channel_ed, 32); at += 32; if (sc_ed25519_sign(t->channel_private, message, at, signature)) return -1; for (unsigned i = 0; i < NODE_COUNT; i++) { if (topo_node_sqlite_channel_put(t->nodes[i]->topo_sqlite_db, CHANNEL, "member-files", t->ids[AUTHOR], t->channel_x, NULL, t->channel_ed, i == AUTHOR ? t->channel_private : NULL, signature)) return -1; chat_core_ensure_channel_ready(t->nodes[i], CHANNEL); for (unsigned j = 0; j < NODE_COUNT; j++) if (put_member(t, i, j, 0)) return -1; } snprintf(t->source, sizeof(t->source), "%s/source.bin", t->dir); snprintf(t->destination, sizeof(t->destination), "%s/download.bin", t->dir); FILE* file = fopen(t->source, "wb"); if (!file) return -1; for (unsigned i = 0; i < BYTES; i++) if (fputc((i * 73 + 19) & 255, file) == EOF) { fclose(file); return -1; } return fclose(file); } static void transfer(struct network_test* t, int upload, uint64_t id) { struct mf_transfer_request* q = u_calloc(1, sizeof(*q)); if (!q) { fail(t, "transfer allocation"); return; } q->inst = t->nodes[upload ? AUTHOR : READER]; q->group = GROUP; q->server = t->ids[SERVER]; q->owner = t->ids[AUTHOR]; q->id = id; q->upload = upload; snprintf(q->remote, sizeof(q->remote), "file.bin"); snprintf(q->local, sizeof(q->local), "%s", upload ? t->source : t->destination); t->finished = 0; member_files_gui_transfer(q); } static void request(struct network_test* t, uint8_t command, uint64_t owner) { struct mf_message q = { .command = command, .owner = owner }; if (owner) snprintf(q.path, sizeof(q.path), "file.bin"); t->answered = 0; if (!member_files_request(t->nodes[READER], GROUP, t->ids[SERVER], &q, response, t)) fail(t, "request start"); } static int downloaded(struct network_test* t) { FILE* file = fopen(t->destination, "rb"); if (!file) return 0; int ok = 1; for (unsigned i = 0; i < BYTES; i++) if (fgetc(file) != (int)((i * 73 + 19) & 255)) { ok = 0; break; } if (fgetc(file) != EOF) ok = 0; fclose(file); return ok; } static void tick(void* arg) { struct network_test* t = arg; t->timer = NULL; if (++t->ticks > 6000) { fail(t, "60s deadline"); return; } if (t->phase == 0) { int a = etcp_router_get_path(t->nodes[AUTHOR], GROUP, t->ids[SERVER], ETCP_RT_ID_MEMBER_FILES).next_hop_node_id != 0; int b = etcp_router_get_path(t->nodes[READER], GROUP, t->ids[SERVER], ETCP_RT_ID_MEMBER_FILES).next_hop_node_id != 0; if (a && b) { t->phase = 1; transfer(t, 1, 1); } } else if (t->phase == 1 && t->finished) { if (t->transfer_status) { fail(t, mf_status_text(t->transfer_status)); return; } t->phase = 2; request(t, MF_STAT, t->ids[AUTHOR]); } else if (t->phase == 2 && t->answered) { if (t->response.status || t->response.size != BYTES) { fail(t, "STAT after upload"); return; } t->phase = 3; request(t, MF_BEGIN_WRITE, t->ids[AUTHOR]); } else if (t->phase == 3 && t->answered) { if (t->response.status != MF_ACCESS_DENIED) { fail(t, "foreign write accepted"); return; } t->phase = 4; transfer(t, 0, 2); } else if (t->phase == 4 && t->finished) { if (t->transfer_status || !downloaded(t)) { fail(t, "download bytes"); return; } t->phase = 5; request(t, MF_LIST, 0); } else if (t->phase == 5 && t->answered) { if (t->response.status || !t->response.data_len) { fail(t, "virtual group root"); return; } t->phase = 6; t->answered = 0; struct mf_message q = { .command = MF_STAT, .owner = t->ids[AUTHOR] }; snprintf(q.path, sizeof(q.path), "file.bin"); if (!member_files_request(t->nodes[SERVER], GROUP, t->ids[SERVER], &q, response, t)) { fail(t, "local server request"); return; } } else if (t->phase == 6 && t->answered) { if (t->response.status || t->response.size != BYTES) { fail(t, "local server STAT"); return; } if (put_member(t, SERVER, READER, 1)) { fail(t, "signed deletion"); return; } t->phase = 7; request(t, MF_STAT, t->ids[AUTHOR]); } else if (t->phase == 7 && t->answered) { if (t->response.status != MF_ACCESS_DENIED) { fail(t, "deleted reader accepted"); return; } DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files network: PASS progress=%u ticks=%u", t->progress, t->ticks); uasync_stop(t->ua); return; } t->timer = uasync_set_timeout(t->ua, 100, t, tick, "member_files_test"); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_TRACE); utun_instance_set_tun_init_enabled(0); struct network_test t = {0}; active = &t; snprintf(t.dir, sizeof(t.dir), "/tmp/utun_member_files_net_XXXXXX"); if (test_mkdtemp(t.dir)) return 1; t.ua = uasync_create(); for (unsigned i = 0; i < NODE_COUNT; i++) sc_generate_keypair(&t.keys[i]); int port = 39000 + (int)(get_time_tb() % 8000); for (unsigned i = 0; i < NODE_COUNT; i++) if (start(&t, i, port)) { t.failed = 1; goto stop; } if (setup(&t)) { t.failed = 1; goto stop; } t.timer = uasync_set_timeout(t.ua, 100, &t, tick, "member_files_test"); uasync_mainloop(t.ua); stop: if (t.timer) uasync_cancel_timeout(t.ua, t.timer); for (unsigned i = 0; i < NODE_COUNT; i++) if (t.nodes[i]) utun_instance_destroy(t.nodes[i]); uasync_destroy(t.ua, 0); printf("member_files network: %s phase=%u\n", t.failed ? "FAIL" : "PASS", t.phase); return t.failed; }