call: P2P аудиозвонки — сигналинг, релей медиа, аудио-контур, headless/desktop/android
- src/call/: сигналинг (call.c, call_proto.h, ETCP_RT_ID_CALL 0x36) + релей медиа + headless аудио-сокет (call_headless.c, call_audio_bind) + Opus/jitter/тоны - chat: события CHAT_EVT_CALL_* (34–40), CHAT_EVT_DM_MESSAGES (41), DM-трамплины - chatgui: call window, miniaudio I/O, audio_frame_ring, test_audio_jitter - chatgui-android: CallScreen/IncomingCallDialog, аудио-роутинг, JNI, рингтон - tests: test_call, test_call_headless
This commit is contained in:
1 parent
0157f7f96b
commit
b11e77e4c0
78 files changed
+7053
-70
No files matched your search
@@ -63,6 +63,8 @@ check_PROGRAMS = \
|
||||
test_chat_join \
|
||||
test_chat_join_e2e \
|
||||
test_dm_e2e \
|
||||
test_call \
|
||||
test_call_headless \
|
||||
test_stcp_traffic \
|
||||
test_bbr_integration \
|
||||
test_audio_compressor \
|
||||
@@ -387,6 +389,14 @@ test_dm_e2e_SOURCES = test_dm_e2e.c
|
||||
test_dm_e2e_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
|
||||
test_dm_e2e_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
|
||||
test_call_SOURCES = test_call.c
|
||||
test_call_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/call -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
|
||||
test_call_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
|
||||
test_call_headless_SOURCES = test_call_headless.c
|
||||
test_call_headless_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/call -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
|
||||
test_call_headless_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
|
||||
test_stcp_traffic_SOURCES = test_stcp_traffic.c
|
||||
test_stcp_traffic_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
|
||||
test_stcp_traffic_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
// test_call.c — интеграционный тест P2P аудио-звонка (сигналинг).
|
||||
//
|
||||
// Однопоточный: 2 узла (A=caller, B=callee) в одном процессе, один UASYNC.
|
||||
// Прямое соединение A←B, оба в CHAT-группе. Master state-machine на таймере.
|
||||
//
|
||||
// Сценарий:
|
||||
// 1. happy path: A call_start → B INCOMING → B accept → обе ACTIVE (ACCEPTED)
|
||||
// → A hangup → обе ENDED.
|
||||
// 2. decline: A call_start → B INCOMING → B decline → обе ENDED(DECLINE).
|
||||
|
||||
#include "chat_core.h"
|
||||
#include "chat_sync.h"
|
||||
#include "chat_event.h"
|
||||
#include "member_sync.h"
|
||||
#include "../call/call.h"
|
||||
#include "../call/call_proto.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../transport_layer/etcp.h"
|
||||
#include "../transport_layer/secure_channel.h"
|
||||
#include "../ntp_time.h"
|
||||
#include "../lib/u_async.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
#define OPENSSL_API_COMPAT 0x10100000L
|
||||
#include <openssl/evp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
|
||||
#define CH_ID "4242424242424242"
|
||||
|
||||
#define IDX_A 0
|
||||
#define IDX_B 1
|
||||
|
||||
#define TICK_TB 100 /* 10 ms на такт */
|
||||
#define MAX_TICKS 3000 /* глобальный таймаут ≈ 30 c */
|
||||
|
||||
enum call_phase {
|
||||
P_WAIT_CONN,
|
||||
P_SETUP,
|
||||
P_WAIT_BGP,
|
||||
P_CALL1_START, /* A: call_start */
|
||||
P_WAIT_INCOMING1, /* B: INCOMING */
|
||||
P_ACCEPT, /* B: call_accept */
|
||||
P_WAIT_ACCEPTED, /* обе ACTIVE */
|
||||
P_SEND_MEDIA, /* A: call_send_media (тестовый Opus-кадр) */
|
||||
P_WAIT_MEDIA, /* B: media_recv_cb получил кадр */
|
||||
P_HANGUP, /* A: call_hangup */
|
||||
P_WAIT_ENDED1, /* обе ENDED */
|
||||
P_CALL2_START, /* A: call_start (2-й звонок) */
|
||||
P_WAIT_INCOMING2, /* B: INCOMING */
|
||||
P_DECLINE, /* B: call_decline */
|
||||
P_WAIT_ENDED2, /* обе ENDED + A DECLINED */
|
||||
P_DONE,
|
||||
};
|
||||
|
||||
struct call_ctx {
|
||||
struct UASYNC* ua;
|
||||
struct UTUN_INSTANCE* inst[2];
|
||||
enum call_phase phase;
|
||||
int result; /* 0=running, 1=ok, 2=fail */
|
||||
int ticks;
|
||||
void* timer;
|
||||
uint64_t call_id; /* текущий call_id */
|
||||
};
|
||||
|
||||
/* записи событий CHAT_EVT_CALL_* на узел */
|
||||
struct call_evt {
|
||||
int incoming, ringing, accepted, ended, declined;
|
||||
uint64_t incoming_call_id;
|
||||
uint64_t accepted_call_id;
|
||||
uint64_t ended_call_id;
|
||||
uint8_t ended_reason;
|
||||
};
|
||||
|
||||
static struct call_evt g_ev[2]; /* [A], [B] */
|
||||
|
||||
/* записи принятых медиа-кадров (media_recv_cb) */
|
||||
struct media_evt {
|
||||
int frames;
|
||||
uint64_t last_call_id;
|
||||
int last_len;
|
||||
uint8_t last[64];
|
||||
};
|
||||
static struct media_evt g_media[2];
|
||||
|
||||
static void media_recv_cb(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
const uint8_t* opus, int len, void* arg) {
|
||||
(void)inst;
|
||||
struct media_evt* m = (struct media_evt*)arg;
|
||||
if (!m) return;
|
||||
m->frames++;
|
||||
m->last_call_id = call_id;
|
||||
m->last_len = len;
|
||||
if (len > 0) memcpy(m->last, opus, (size_t)(len < 64 ? len : 64));
|
||||
}
|
||||
|
||||
struct call_shared {
|
||||
uint8_t ch_x_pub[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64];
|
||||
uint64_t nid[2];
|
||||
uint8_t x_pub[2][32], x_priv[2][32], ed_pub[2][32];
|
||||
int port[2];
|
||||
};
|
||||
static struct call_shared g_sh;
|
||||
|
||||
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0;
|
||||
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0)
|
||||
#define OK() do { G_PASSED++; printf("OK\n"); } while(0)
|
||||
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0)
|
||||
|
||||
/* ── крипто-хелперы ── */
|
||||
|
||||
static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) {
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
|
||||
EVP_PKEY* pkey = NULL;
|
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
|
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
|
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) {
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
|
||||
EVP_PKEY* pkey = NULL;
|
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
|
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
|
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
static void to_hex(const uint8_t* bin, size_t n, char* out) {
|
||||
for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]);
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
static int wf(const char* p, const char* f, ...) {
|
||||
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1;
|
||||
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0;
|
||||
}
|
||||
|
||||
/* ── генерация данных сценария + конфигов ── */
|
||||
|
||||
static void fill_shared(int base_port) {
|
||||
memset(&g_sh, 0, sizeof(g_sh));
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]);
|
||||
sc_derive_ed25519_pubkey(g_sh.x_priv[i], g_sh.ed_pub[i]);
|
||||
g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]);
|
||||
g_sh.port[i] = base_port + i * 1000;
|
||||
}
|
||||
|
||||
{ uint8_t ch_x_priv[32]; gen_x25519(g_sh.ch_x_pub, ch_x_priv); }
|
||||
gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv);
|
||||
|
||||
{
|
||||
uint8_t msg[256]; size_t off = 0;
|
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_ID) + 1;
|
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", "call-test") + 1;
|
||||
memcpy(msg + off, &g_sh.nid[IDX_A], 8); off += 8;
|
||||
memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32;
|
||||
memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32;
|
||||
sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig);
|
||||
}
|
||||
}
|
||||
|
||||
static void write_configs(const char* dir) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char priv[65], pub[65], path[512], dbd[512];
|
||||
to_hex(g_sh.x_priv[i], 32, priv);
|
||||
to_hex(g_sh.x_pub[i], 32, pub);
|
||||
snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : "b");
|
||||
snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : "b");
|
||||
utun_mkdir(dbd, 0755);
|
||||
|
||||
char client[512] = "";
|
||||
if (i == IDX_B) {
|
||||
/* B → A (клиент к A) */
|
||||
char apub[65]; to_hex(g_sh.x_pub[IDX_A], 32, apub);
|
||||
snprintf(client, sizeof(client),
|
||||
"[client: to_a]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n",
|
||||
apub, g_sh.port[IDX_A]);
|
||||
}
|
||||
|
||||
wf(path,
|
||||
"[global]\ntun_ip=10.97.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n"
|
||||
"my_private_key=%s\nmy_public_key=%s\n"
|
||||
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n"
|
||||
"%s"
|
||||
"[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n",
|
||||
i, i, dir, i == IDX_A ? "a" : "b",
|
||||
priv, pub, g_sh.port[i], client);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── событийный обработчик ── */
|
||||
|
||||
static void call_evt_handler(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
||||
struct call_evt* ev = (struct call_evt*)inst->test_user_ptr;
|
||||
if (!ev) return;
|
||||
switch (type) {
|
||||
case CHAT_EVT_CALL_INCOMING: {
|
||||
if (len < 1 + 8 + 8) return;
|
||||
int ch_len = data[0];
|
||||
if (len < 1 + ch_len + 8 + 8) return;
|
||||
ev->incoming = 1;
|
||||
memcpy(&ev->incoming_call_id, data + 1 + ch_len + 8, 8);
|
||||
break;
|
||||
}
|
||||
case CHAT_EVT_CALL_RINGING:
|
||||
ev->ringing = 1;
|
||||
break;
|
||||
case CHAT_EVT_CALL_ACCEPTED:
|
||||
ev->accepted = 1;
|
||||
if (len >= 8) memcpy(&ev->accepted_call_id, data, 8);
|
||||
break;
|
||||
case CHAT_EVT_CALL_ENDED:
|
||||
ev->ended = 1;
|
||||
if (len >= 9) { memcpy(&ev->ended_call_id, data, 8); ev->ended_reason = data[8]; }
|
||||
break;
|
||||
case CHAT_EVT_CALL_DECLINED:
|
||||
ev->declined = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── проверки ── */
|
||||
|
||||
static int conn_up(struct UTUN_INSTANCE* inst, uint64_t nid) {
|
||||
struct ETCP_CONN* c = instance_find_conn(inst, nid);
|
||||
return c && c->initialized && c->links_up;
|
||||
}
|
||||
|
||||
static int bgp_has(struct UTUN_INSTANCE* inst, uint64_t nid) {
|
||||
uint64_t gid = strtoull(CH_ID, NULL, 10);
|
||||
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
|
||||
if (!g) return 0;
|
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid);
|
||||
return nq && nq->paths && nq->paths->head;
|
||||
}
|
||||
|
||||
/* ── настройка канала и мемберов ── */
|
||||
|
||||
static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) {
|
||||
topo_node_sqlite_channel_put(inst->topo_sqlite_db, CH_ID, "call-test", g_sh.nid[IDX_A],
|
||||
g_sh.ch_x_pub, NULL, g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig);
|
||||
chat_core_ensure_channel_ready(inst, CH_ID);
|
||||
}
|
||||
|
||||
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
|
||||
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
|
||||
uint8_t jm[128];
|
||||
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id,
|
||||
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm));
|
||||
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig);
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
||||
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
}
|
||||
|
||||
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) {
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx],
|
||||
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
}
|
||||
|
||||
static void do_setup(struct UTUN_INSTANCE* inst, int role) {
|
||||
switch (role) {
|
||||
case IDX_A:
|
||||
channel_put_shared(inst, 1);
|
||||
member_put_self(inst, "A");
|
||||
member_put_placeholder(inst, IDX_B, "B");
|
||||
member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL);
|
||||
break;
|
||||
case IDX_B:
|
||||
channel_put_shared(inst, 0);
|
||||
member_put_self(inst, "B");
|
||||
member_put_placeholder(inst, IDX_A, "A");
|
||||
member_sync_start(inst, g_sh.nid[IDX_A], CH_ID, NULL, NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── master state-machine ── */
|
||||
|
||||
static void call_tick(void* arg) {
|
||||
struct call_ctx* t = arg;
|
||||
t->timer = NULL;
|
||||
if (t->result) return;
|
||||
|
||||
struct UTUN_INSTANCE* A = t->inst[IDX_A];
|
||||
struct UTUN_INSTANCE* B = t->inst[IDX_B];
|
||||
uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B];
|
||||
struct call_evt* ea = &g_ev[IDX_A];
|
||||
struct call_evt* eb = &g_ev[IDX_B];
|
||||
|
||||
switch (t->phase) {
|
||||
case P_WAIT_CONN:
|
||||
if (conn_up(A, nb) && conn_up(B, na)) t->phase = P_SETUP;
|
||||
break;
|
||||
case P_SETUP:
|
||||
do_setup(A, IDX_A);
|
||||
do_setup(B, IDX_B);
|
||||
t->phase = P_WAIT_BGP;
|
||||
break;
|
||||
case P_WAIT_BGP:
|
||||
if (bgp_has(A, nb) && bgp_has(B, na)) t->phase = P_CALL1_START;
|
||||
break;
|
||||
case P_CALL1_START:
|
||||
memset(ea, 0, sizeof(*ea));
|
||||
memset(eb, 0, sizeof(*eb));
|
||||
t->call_id = ((uint64_t)1 << 40) | (uint64_t)(rand() & 0xFFFFF);
|
||||
call_start(A, CH_ID, nb, t->call_id);
|
||||
t->phase = P_WAIT_INCOMING1;
|
||||
break;
|
||||
case P_WAIT_INCOMING1:
|
||||
if (eb->incoming && eb->incoming_call_id == t->call_id) t->phase = P_ACCEPT;
|
||||
break;
|
||||
case P_ACCEPT:
|
||||
call_accept(B, t->call_id);
|
||||
t->phase = P_WAIT_ACCEPTED;
|
||||
break;
|
||||
case P_WAIT_ACCEPTED:
|
||||
if (ea->accepted && eb->accepted && ea->accepted_call_id == t->call_id) t->phase = P_SEND_MEDIA;
|
||||
break;
|
||||
case P_SEND_MEDIA: {
|
||||
/* тестовый «Opus-кадр» — релей произвольных байт, не валидируем кодек */
|
||||
static const uint8_t opus[16] = { 0xf8, 0xff, 0xfe, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d };
|
||||
if (call_send_media(A, t->call_id, opus, sizeof(opus)) != 0) {
|
||||
fprintf(stderr, "A: call_send_media failed\n");
|
||||
t->result = 2;
|
||||
} else {
|
||||
t->phase = P_WAIT_MEDIA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case P_WAIT_MEDIA:
|
||||
if (g_media[IDX_B].frames > 0 && g_media[IDX_B].last_call_id == t->call_id
|
||||
&& g_media[IDX_B].last_len == 16) t->phase = P_HANGUP;
|
||||
break;
|
||||
case P_HANGUP:
|
||||
call_hangup(A, t->call_id);
|
||||
t->phase = P_WAIT_ENDED1;
|
||||
break;
|
||||
case P_WAIT_ENDED1:
|
||||
if (ea->ended && eb->ended && ea->ended_call_id == t->call_id) t->phase = P_CALL2_START;
|
||||
break;
|
||||
case P_CALL2_START:
|
||||
memset(ea, 0, sizeof(*ea));
|
||||
memset(eb, 0, sizeof(*eb));
|
||||
t->call_id = ((uint64_t)2 << 40) | (uint64_t)(rand() & 0xFFFFF);
|
||||
call_start(A, CH_ID, nb, t->call_id);
|
||||
t->phase = P_WAIT_INCOMING2;
|
||||
break;
|
||||
case P_WAIT_INCOMING2:
|
||||
if (eb->incoming && eb->incoming_call_id == t->call_id) t->phase = P_DECLINE;
|
||||
break;
|
||||
case P_DECLINE:
|
||||
call_decline(B, t->call_id);
|
||||
t->phase = P_WAIT_ENDED2;
|
||||
break;
|
||||
case P_WAIT_ENDED2:
|
||||
if (ea->ended && eb->ended && ea->declined) t->phase = P_DONE;
|
||||
break;
|
||||
case P_DONE:
|
||||
t->result = 1;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t->phase == P_DONE) { t->result = 1; uasync_stop(t->ua); return; }
|
||||
|
||||
if (++t->ticks > MAX_TICKS) {
|
||||
fprintf(stderr, "test: timeout in phase %d (A_conn=%d B_conn=%d evA:inc=%d acc=%d end=%d dec=%d evB:inc=%d acc=%d end=%d dec=%d)\n",
|
||||
(int)t->phase, conn_up(A, nb), conn_up(B, na),
|
||||
ea->incoming, ea->accepted, ea->ended, ea->declined,
|
||||
eb->incoming, eb->accepted, eb->ended, eb->declined);
|
||||
t->result = 2;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, call_tick, "call_tick");
|
||||
}
|
||||
|
||||
/* ── main ── */
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc; (void)argv;
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
||||
if (getenv("UTUN_TEST_DEBUG")) {
|
||||
debug_set_category_level_by_name("call", "trace");
|
||||
debug_set_category_level_by_name("bgp", "info");
|
||||
debug_set_category_level_by_name("etcp_route", "info");
|
||||
debug_set_category_level_by_name("member_sync", "info");
|
||||
debug_set_category_level_by_name("chat_sync", "info");
|
||||
}
|
||||
utun_instance_set_tun_init_enabled(0);
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_call_XXXXXX");
|
||||
if (!mkdtemp(dir)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
|
||||
setenv("UTUN_TEST_DIR", dir, 1);
|
||||
|
||||
fill_shared(56000 + (getpid() % 2000));
|
||||
write_configs(dir);
|
||||
|
||||
printf("=== test_call ===\n"); fflush(stdout);
|
||||
|
||||
TEST("single-thread: call happy path + decline (2 nodes)"); {
|
||||
struct UASYNC* ua = uasync_create();
|
||||
if (!ua) { FAIL("uasync_create failed"); return 1; }
|
||||
|
||||
struct call_ctx t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.ua = ua;
|
||||
t.phase = P_WAIT_CONN;
|
||||
|
||||
char cfgA[512], cfgB[512];
|
||||
snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir);
|
||||
snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir);
|
||||
|
||||
t.inst[IDX_A] = utun_instance_create(ua, cfgA);
|
||||
t.inst[IDX_B] = utun_instance_create(ua, cfgB);
|
||||
if (!t.inst[IDX_A] || !t.inst[IDX_B]) {
|
||||
FAIL("instance create failed A=%p B=%p", (void*)t.inst[IDX_A], (void*)t.inst[IDX_B]);
|
||||
uasync_destroy(ua, 0);
|
||||
return 1;
|
||||
}
|
||||
utun_instance_init(t.inst[IDX_A]);
|
||||
utun_instance_init(t.inst[IDX_B]);
|
||||
|
||||
t.inst[IDX_A]->test_user_ptr = &g_ev[IDX_A];
|
||||
t.inst[IDX_B]->test_user_ptr = &g_ev[IDX_B];
|
||||
chat_event_set_handler(t.inst[IDX_A], call_evt_handler);
|
||||
chat_event_set_handler(t.inst[IDX_B], call_evt_handler);
|
||||
call_set_media_recv_cb(t.inst[IDX_A], media_recv_cb, &g_media[IDX_A]);
|
||||
call_set_media_recv_cb(t.inst[IDX_B], media_recv_cb, &g_media[IDX_B]);
|
||||
|
||||
t.timer = uasync_set_timeout(ua, TICK_TB, &t, call_tick, "call_tick");
|
||||
uasync_mainloop(ua);
|
||||
|
||||
int ok = (t.result == 1);
|
||||
|
||||
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
|
||||
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);
|
||||
}
|
||||
|
||||
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd);
|
||||
|
||||
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
|
||||
return G_FAILED > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
// test_call_headless.c — полноценный e2e-тест звонка через headless API.
|
||||
//
|
||||
// Два узла (A=caller, B=callee) в одном процессе, один UASYNC. Тест выступает
|
||||
// «headless-клиентом» обоих узлов: подключается к control-сокетам (JSON-команды
|
||||
// и события) и аудио-сокетам (бинарные Opus-фреймы) каждого узла.
|
||||
//
|
||||
// Сценарий (через реальные TCP-сокеты):
|
||||
// A: call_start → B: call_incoming → B: call_accept → обе: call_accepted
|
||||
// A: audio HELLO+FRAME → B: audio FRAME (релей медиа)
|
||||
// A: call_hangup → обе: call_ended
|
||||
|
||||
#include "chat_core.h"
|
||||
#include "chat_sync.h"
|
||||
#include "chat_event.h"
|
||||
#include "member_sync.h"
|
||||
#include "chat_headless_control.h"
|
||||
#include "../call/call.h"
|
||||
#include "../call/call_headless.h"
|
||||
#include "../call/call_proto.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../transport_layer/etcp.h"
|
||||
#include "../transport_layer/secure_channel.h"
|
||||
#include "../ntp_time.h"
|
||||
#include "../lib/u_async.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/socket_compat.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
#define OPENSSL_API_COMPAT 0x10100000L
|
||||
#include <openssl/evp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define CH_ID "4242424242424242"
|
||||
|
||||
#define IDX_A 0
|
||||
#define IDX_B 1
|
||||
|
||||
#define TICK_TB 100 /* 10 ms на такт */
|
||||
#define MAX_TICKS 4000 /* глобальный таймаут ≈ 40 c */
|
||||
|
||||
enum hs_phase {
|
||||
P_WAIT_CONN,
|
||||
P_SETUP, /* канал/мемберы + headless control/audio на A и B */
|
||||
P_WAIT_BGP,
|
||||
P_CONNECT_CLIENTS, /* подключить 4 клиентских сокета */
|
||||
P_SUBSCRIBE, /* subscribe на обоих control */
|
||||
P_CALL_START, /* A: call_start */
|
||||
P_WAIT_INCOMING, /* B: call_incoming */
|
||||
P_ACCEPT, /* B: call_accept */
|
||||
P_WAIT_ACCEPTED, /* обе: call_accepted */
|
||||
P_BIND_AUDIO, /* HELLO на обоих audio */
|
||||
P_SEND_MEDIA, /* A: audio FRAME */
|
||||
P_WAIT_MEDIA, /* B: audio FRAME получен */
|
||||
P_HANGUP, /* A: call_hangup */
|
||||
P_WAIT_ENDED, /* обе: call_ended */
|
||||
P_DONE,
|
||||
};
|
||||
|
||||
/* клиентский сокет (к control или audio узла) */
|
||||
struct hs_conn {
|
||||
socket_t fd;
|
||||
void* socket_id;
|
||||
char buf[16384];
|
||||
int len;
|
||||
};
|
||||
|
||||
struct hs_ctx {
|
||||
struct UASYNC* ua;
|
||||
struct UTUN_INSTANCE* inst[2];
|
||||
enum hs_phase phase;
|
||||
int result;
|
||||
int ticks;
|
||||
void* timer;
|
||||
uint64_t call_id;
|
||||
struct hs_conn ctl[2]; /* control-клиенты A, B */
|
||||
struct hs_conn aud[2]; /* audio-клиенты A, B */
|
||||
};
|
||||
|
||||
struct hs_shared {
|
||||
uint8_t ch_x_pub[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64];
|
||||
uint64_t nid[2];
|
||||
uint8_t x_pub[2][32], x_priv[2][32], ed_pub[2][32];
|
||||
int port[2]; /* etcp-порты */
|
||||
};
|
||||
static struct hs_shared g_sh;
|
||||
|
||||
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0;
|
||||
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0)
|
||||
#define OK() do { G_PASSED++; printf("OK\n"); } while(0)
|
||||
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0)
|
||||
|
||||
/* ── крипто-хелперы ── */
|
||||
|
||||
static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) {
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
|
||||
EVP_PKEY* pkey = NULL;
|
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
|
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
|
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) {
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
|
||||
EVP_PKEY* pkey = NULL;
|
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
|
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
|
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
static void to_hex(const uint8_t* bin, size_t n, char* out) {
|
||||
for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]);
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
static int wf(const char* p, const char* f, ...) {
|
||||
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1;
|
||||
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0;
|
||||
}
|
||||
|
||||
static void fill_shared(int base_port) {
|
||||
memset(&g_sh, 0, sizeof(g_sh));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]);
|
||||
sc_derive_ed25519_pubkey(g_sh.x_priv[i], g_sh.ed_pub[i]);
|
||||
g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]);
|
||||
g_sh.port[i] = base_port + i * 1000;
|
||||
}
|
||||
{ uint8_t ch_x_priv[32]; gen_x25519(g_sh.ch_x_pub, ch_x_priv); }
|
||||
gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv);
|
||||
{
|
||||
uint8_t msg[256]; size_t off = 0;
|
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_ID) + 1;
|
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", "hl-call") + 1;
|
||||
memcpy(msg + off, &g_sh.nid[IDX_A], 8); off += 8;
|
||||
memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32;
|
||||
memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32;
|
||||
sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig);
|
||||
}
|
||||
}
|
||||
|
||||
static void write_configs(const char* dir) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char priv[65], pub[65], path[512], dbd[512];
|
||||
to_hex(g_sh.x_priv[i], 32, priv);
|
||||
to_hex(g_sh.x_pub[i], 32, pub);
|
||||
snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : "b");
|
||||
snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : "b");
|
||||
utun_mkdir(dbd, 0755);
|
||||
|
||||
char client[512] = "";
|
||||
if (i == IDX_B) {
|
||||
char apub[65]; to_hex(g_sh.x_pub[IDX_A], 32, apub);
|
||||
snprintf(client, sizeof(client),
|
||||
"[client: to_a]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n",
|
||||
apub, g_sh.port[IDX_A]);
|
||||
}
|
||||
wf(path,
|
||||
"[global]\ntun_ip=10.97.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n"
|
||||
"my_private_key=%s\nmy_public_key=%s\n"
|
||||
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n"
|
||||
"%s"
|
||||
"[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n",
|
||||
i, i, dir, i == IDX_A ? "a" : "b",
|
||||
priv, pub, g_sh.port[i], client);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── проверки ── */
|
||||
|
||||
static int conn_up(struct UTUN_INSTANCE* inst, uint64_t nid) {
|
||||
struct ETCP_CONN* c = instance_find_conn(inst, nid);
|
||||
return c && c->initialized && c->links_up;
|
||||
}
|
||||
|
||||
static int bgp_has(struct UTUN_INSTANCE* inst, uint64_t nid) {
|
||||
uint64_t gid = strtoull(CH_ID, NULL, 10);
|
||||
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
|
||||
if (!g) return 0;
|
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid);
|
||||
return nq && nq->paths && nq->paths->head;
|
||||
}
|
||||
|
||||
/* ── настройка канала и мемберов ── */
|
||||
|
||||
static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) {
|
||||
topo_node_sqlite_channel_put(inst->topo_sqlite_db, CH_ID, "hl-call", g_sh.nid[IDX_A],
|
||||
g_sh.ch_x_pub, NULL, g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig);
|
||||
chat_core_ensure_channel_ready(inst, CH_ID);
|
||||
}
|
||||
|
||||
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
|
||||
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
|
||||
uint8_t jm[128];
|
||||
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id,
|
||||
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm));
|
||||
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig);
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
||||
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
}
|
||||
|
||||
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) {
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx],
|
||||
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
}
|
||||
|
||||
static void do_setup(struct UTUN_INSTANCE* inst, int role) {
|
||||
switch (role) {
|
||||
case IDX_A:
|
||||
channel_put_shared(inst, 1);
|
||||
member_put_self(inst, "A");
|
||||
member_put_placeholder(inst, IDX_B, "B");
|
||||
member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL);
|
||||
break;
|
||||
case IDX_B:
|
||||
channel_put_shared(inst, 0);
|
||||
member_put_self(inst, "B");
|
||||
member_put_placeholder(inst, IDX_A, "A");
|
||||
member_sync_start(inst, g_sh.nid[IDX_A], CH_ID, NULL, NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── клиентские сокеты ── */
|
||||
|
||||
static void hs_recv_cb(socket_t fd, void* arg) {
|
||||
struct hs_conn* c = (struct hs_conn*)arg;
|
||||
(void)fd;
|
||||
char tmp[4096];
|
||||
ssize_t r = recv(fd, tmp, sizeof(tmp) - 1, 0);
|
||||
if (r <= 0) return;
|
||||
if (c->len + (int)r < (int)sizeof(c->buf)) {
|
||||
memcpy(c->buf + c->len, tmp, (size_t)r);
|
||||
c->len += (int)r;
|
||||
c->buf[c->len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static int hs_connect(struct hs_ctx* t, struct hs_conn* c, int port) {
|
||||
c->fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (c->fd == SOCKET_INVALID) return -1;
|
||||
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
|
||||
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
|
||||
inet_pton(AF_INET, "127.0.0.1", &sin.sin_addr);
|
||||
if (connect(c->fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
|
||||
socket_close_wrapper(c->fd); c->fd = SOCKET_INVALID; return -1;
|
||||
}
|
||||
socket_set_nonblocking(c->fd);
|
||||
c->socket_id = uasync_add_socket_t(t->ua, c->fd, hs_recv_cb, NULL, NULL, c);
|
||||
if (!c->socket_id) { socket_close_wrapper(c->fd); c->fd = SOCKET_INVALID; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void hs_send(struct hs_conn* c, const void* data, size_t len) {
|
||||
if (!c || c->fd == SOCKET_INVALID) return;
|
||||
ssize_t off = 0;
|
||||
while (off < (ssize_t)len) {
|
||||
ssize_t r = send(c->fd, (const char*)data + off, len - (size_t)off, 0);
|
||||
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) { continue; }
|
||||
if (r <= 0) break;
|
||||
off += r;
|
||||
}
|
||||
}
|
||||
|
||||
/* есть ли подстрока в буфере */
|
||||
static int hs_has(struct hs_conn* c, const char* substr) {
|
||||
return c && strstr(c->buf, substr) != NULL;
|
||||
}
|
||||
|
||||
/* вытащить call_id из JSON-события (формат "call_id":"0x...") */
|
||||
static int hs_extract_call_id(struct hs_conn* c, const char* evname, uint64_t* out) {
|
||||
if (!c) return -1;
|
||||
const char* p = strstr(c->buf, evname);
|
||||
if (!p) return -1;
|
||||
p = strstr(p, "\"call_id\"");
|
||||
if (!p) return -1;
|
||||
p = strstr(p, "0x");
|
||||
if (!p) return -1;
|
||||
*out = strtoull(p, NULL, 16);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── master state-machine ── */
|
||||
|
||||
static void hs_tick(void* arg) {
|
||||
struct hs_ctx* t = arg;
|
||||
t->timer = NULL;
|
||||
if (t->result) return;
|
||||
|
||||
struct UTUN_INSTANCE* A = t->inst[IDX_A];
|
||||
struct UTUN_INSTANCE* B = t->inst[IDX_B];
|
||||
uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B];
|
||||
|
||||
switch (t->phase) {
|
||||
case P_WAIT_CONN:
|
||||
if (conn_up(A, nb) && conn_up(B, na)) t->phase = P_SETUP;
|
||||
break;
|
||||
case P_SETUP:
|
||||
do_setup(A, IDX_A);
|
||||
do_setup(B, IDX_B);
|
||||
if (chat_headless_control_init(A->ua, A, "127.0.0.1", 9700 + (int)(getpid() % 100)) != 0
|
||||
|| chat_headless_control_init(B->ua, B, "127.0.0.1", 9800 + (int)(getpid() % 100)) != 0) {
|
||||
fprintf(stderr, "headless control init failed\n"); t->result = 2; break;
|
||||
}
|
||||
if (call_headless_init(A->ua, A, "127.0.0.1", 9710 + (int)(getpid() % 100)) != 0
|
||||
|| call_headless_init(B->ua, B, "127.0.0.1", 9810 + (int)(getpid() % 100)) != 0) {
|
||||
fprintf(stderr, "call headless audio init failed\n"); t->result = 2; break;
|
||||
}
|
||||
t->phase = P_WAIT_BGP;
|
||||
break;
|
||||
case P_WAIT_BGP:
|
||||
if (bgp_has(A, nb) && bgp_has(B, na)) t->phase = P_CONNECT_CLIENTS;
|
||||
break;
|
||||
case P_CONNECT_CLIENTS:
|
||||
if (hs_connect(t, &t->ctl[IDX_A], 9700 + (int)(getpid() % 100)) != 0
|
||||
|| hs_connect(t, &t->ctl[IDX_B], 9800 + (int)(getpid() % 100)) != 0
|
||||
|| hs_connect(t, &t->aud[IDX_A], 9710 + (int)(getpid() % 100)) != 0
|
||||
|| hs_connect(t, &t->aud[IDX_B], 9810 + (int)(getpid() % 100)) != 0) {
|
||||
fprintf(stderr, "client connect failed\n"); t->result = 2; break;
|
||||
}
|
||||
t->phase = P_SUBSCRIBE;
|
||||
break;
|
||||
case P_SUBSCRIBE: {
|
||||
const char* sub = "{\"id\":1,\"cmd\":\"subscribe\",\"enable\":1}\n";
|
||||
hs_send(&t->ctl[IDX_A], sub, strlen(sub));
|
||||
hs_send(&t->ctl[IDX_B], sub, strlen(sub));
|
||||
t->phase = P_CALL_START;
|
||||
break;
|
||||
}
|
||||
case P_CALL_START: {
|
||||
t->call_id = ((uint64_t)1 << 40) | (uint64_t)(rand() & 0xFFFFF);
|
||||
char json[512];
|
||||
snprintf(json, sizeof(json), "{\"id\":2,\"cmd\":\"call_start\",\"ch\":\"%s\",\"node_id\":\"0x%016llx\"}\n",
|
||||
CH_ID, (unsigned long long)nb);
|
||||
hs_send(&t->ctl[IDX_A], json, strlen(json));
|
||||
t->phase = P_WAIT_INCOMING;
|
||||
break;
|
||||
}
|
||||
case P_WAIT_INCOMING: {
|
||||
uint64_t cid = 0;
|
||||
if (hs_extract_call_id(&t->ctl[IDX_B], "call_incoming", &cid) == 0) {
|
||||
t->call_id = cid;
|
||||
t->phase = P_ACCEPT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case P_ACCEPT: {
|
||||
char json[256];
|
||||
snprintf(json, sizeof(json), "{\"id\":3,\"cmd\":\"call_accept\",\"call_id\":\"0x%016llx\"}\n",
|
||||
(unsigned long long)t->call_id);
|
||||
hs_send(&t->ctl[IDX_B], json, strlen(json));
|
||||
t->phase = P_WAIT_ACCEPTED;
|
||||
break;
|
||||
}
|
||||
case P_WAIT_ACCEPTED:
|
||||
if (hs_has(&t->ctl[IDX_A], "call_accepted") && hs_has(&t->ctl[IDX_B], "call_accepted"))
|
||||
t->phase = P_BIND_AUDIO;
|
||||
break;
|
||||
case P_BIND_AUDIO: {
|
||||
uint8_t hdr[CALL_AUDIO_HDR_SIZE];
|
||||
hdr[0] = CALL_AUDIO_HELLO;
|
||||
memcpy(hdr + 1, &t->call_id, 8);
|
||||
hdr[9] = 0; hdr[10] = 0;
|
||||
hs_send(&t->aud[IDX_A], hdr, sizeof(hdr));
|
||||
hs_send(&t->aud[IDX_B], hdr, sizeof(hdr));
|
||||
t->phase = P_SEND_MEDIA;
|
||||
break;
|
||||
}
|
||||
case P_SEND_MEDIA: {
|
||||
static const uint8_t opus[16] = { 0xf8,0xff,0xfe,1,2,3,4,5,6,7,8,9,0x0a,0x0b,0x0c,0x0d };
|
||||
uint8_t frm[CALL_AUDIO_HDR_SIZE + sizeof(opus)];
|
||||
frm[0] = CALL_AUDIO_FRAME;
|
||||
memcpy(frm + 1, &t->call_id, 8);
|
||||
frm[9] = 0; frm[10] = (uint8_t)sizeof(opus);
|
||||
memcpy(frm + CALL_AUDIO_HDR_SIZE, opus, sizeof(opus));
|
||||
hs_send(&t->aud[IDX_A], frm, sizeof(frm));
|
||||
t->phase = P_WAIT_MEDIA;
|
||||
break;
|
||||
}
|
||||
case P_WAIT_MEDIA: {
|
||||
/* ищем FRAME от пира в буфере audio-клиента B: type=0x02 + call_id */
|
||||
const char* buf = t->aud[IDX_B].buf;
|
||||
int len = t->aud[IDX_B].len;
|
||||
if (len >= CALL_AUDIO_HDR_SIZE) {
|
||||
for (int i = 0; i + CALL_AUDIO_HDR_SIZE <= len; i++) {
|
||||
if ((uint8_t)buf[i] != CALL_AUDIO_FRAME) continue;
|
||||
uint64_t cid; memcpy(&cid, buf + i + 1, 8);
|
||||
uint16_t flen = (uint16_t)(((uint8_t)buf[i+9] << 8) | (uint8_t)buf[i+10]);
|
||||
if (cid == t->call_id && flen == 16 && i + CALL_AUDIO_HDR_SIZE + flen <= len) {
|
||||
t->phase = P_HANGUP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case P_HANGUP: {
|
||||
char json[256];
|
||||
snprintf(json, sizeof(json), "{\"id\":4,\"cmd\":\"call_hangup\",\"call_id\":\"0x%016llx\"}\n",
|
||||
(unsigned long long)t->call_id);
|
||||
hs_send(&t->ctl[IDX_A], json, strlen(json));
|
||||
t->phase = P_WAIT_ENDED;
|
||||
break;
|
||||
}
|
||||
case P_WAIT_ENDED:
|
||||
if (hs_has(&t->ctl[IDX_A], "call_ended") && hs_has(&t->ctl[IDX_B], "call_ended"))
|
||||
t->phase = P_DONE;
|
||||
break;
|
||||
case P_DONE:
|
||||
t->result = 1;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t->phase == P_DONE) { t->result = 1; uasync_stop(t->ua); return; }
|
||||
|
||||
if (++t->ticks > MAX_TICKS) {
|
||||
fprintf(stderr, "test: timeout in phase %d (A_conn=%d B_conn=%d)\n", (int)t->phase,
|
||||
conn_up(A, nb), conn_up(B, na));
|
||||
t->result = 2;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, hs_tick, "hs_tick");
|
||||
}
|
||||
|
||||
/* ── main ── */
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc; (void)argv;
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
||||
if (getenv("UTUN_TEST_DEBUG")) {
|
||||
debug_set_category_level_by_name("call", "trace");
|
||||
debug_set_category_level_by_name("bgp", "info");
|
||||
debug_set_category_level_by_name("etcp_route", "info");
|
||||
debug_set_category_level_by_name("member_sync", "info");
|
||||
debug_set_category_level_by_name("chat_sync", "info");
|
||||
debug_set_category_level_by_name("general", "info");
|
||||
debug_set_category_level_by_name("chat", "info");
|
||||
}
|
||||
utun_instance_set_tun_init_enabled(0);
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_call_hl_XXXXXX");
|
||||
if (!mkdtemp(dir)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
|
||||
setenv("UTUN_TEST_DIR", dir, 1);
|
||||
|
||||
fill_shared(56000 + (getpid() % 2000));
|
||||
write_configs(dir);
|
||||
|
||||
printf("=== test_call_headless ===\n"); fflush(stdout);
|
||||
|
||||
TEST("headless: call_start/accept/hangup + audio stream через сокеты"); {
|
||||
struct UASYNC* ua = uasync_create();
|
||||
if (!ua) { FAIL("uasync_create failed"); return 1; }
|
||||
|
||||
struct hs_ctx t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.ua = ua;
|
||||
t.phase = P_WAIT_CONN;
|
||||
for (int i = 0; i < 2; i++) { t.ctl[i].fd = SOCKET_INVALID; t.aud[i].fd = SOCKET_INVALID; }
|
||||
|
||||
char cfgA[512], cfgB[512];
|
||||
snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir);
|
||||
snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir);
|
||||
|
||||
t.inst[IDX_A] = utun_instance_create(ua, cfgA);
|
||||
t.inst[IDX_B] = utun_instance_create(ua, cfgB);
|
||||
if (!t.inst[IDX_A] || !t.inst[IDX_B]) {
|
||||
FAIL("instance create failed A=%p B=%p", (void*)t.inst[IDX_A], (void*)t.inst[IDX_B]);
|
||||
uasync_destroy(ua, 0);
|
||||
return 1;
|
||||
}
|
||||
utun_instance_init(t.inst[IDX_A]);
|
||||
utun_instance_init(t.inst[IDX_B]);
|
||||
|
||||
t.timer = uasync_set_timeout(ua, TICK_TB, &t, hs_tick, "hs_tick");
|
||||
uasync_mainloop(ua);
|
||||
|
||||
int ok = (t.result == 1);
|
||||
|
||||
/* закрыть клиентские сокеты */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (t.ctl[i].fd != SOCKET_INVALID) { uasync_remove_socket_t(ua, t.ctl[i].fd); socket_close_wrapper(t.ctl[i].fd); }
|
||||
if (t.aud[i].fd != SOCKET_INVALID) { uasync_remove_socket_t(ua, t.aud[i].fd); socket_close_wrapper(t.aud[i].fd); }
|
||||
}
|
||||
|
||||
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
|
||||
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);
|
||||
}
|
||||
|
||||
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd);
|
||||
|
||||
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
|
||||
return G_FAILED > 0 ? 1 : 0;
|
||||
}
|
||||
+155
-3
@@ -7,7 +7,7 @@
|
||||
// Сценарий:
|
||||
// фаза 1 (прямая): A,B,C online. A dm_start(B) → dm_send("hello1") → B получает напрямую.
|
||||
// фаза 2 (offline): B1 завершается (A теряет B). A dm_send("hello2") → недоступен →
|
||||
// кладёт в mailbox на C (dm_storage=yes). C видит блоб в dm_mail.
|
||||
// кладёт в mailbox на C (storage=yes). C видит блоб в dm_mail.
|
||||
// фаза 3 (reconnect): B2 переподключается к C → dm_on_conn_status → PULL mailbox → получает "hello2".
|
||||
|
||||
#include "chat_core.h"
|
||||
@@ -244,10 +244,10 @@ static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const ch
|
||||
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
}
|
||||
|
||||
/* мембер C со флагом dm_storage=yes (adm_tags подписан канальным ключом) */
|
||||
/* мембер C со флагом storage=yes (adm_tags подписан канальным ключом) */
|
||||
static void member_put_storage(struct UTUN_INSTANCE* inst, int idx, const char* name) {
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
char adm_tags[64]; snprintf(adm_tags, sizeof(adm_tags), "{\"dm_storage\":\"yes\",\"ver\":\"1\"}");
|
||||
char adm_tags[64]; snprintf(adm_tags, sizeof(adm_tags), "{\"storage\":\"yes\",\"ver\":\"1\"}");
|
||||
uint8_t ats[64];
|
||||
uint8_t amsg[256]; size_t off = strlen(adm_tags);
|
||||
memcpy(amsg, adm_tags, off);
|
||||
@@ -378,6 +378,108 @@ static void dm_tick(void* arg) {
|
||||
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, dm_tick, "dm_tick");
|
||||
}
|
||||
|
||||
/* ── сценарий 2: первое сообщение в offline (у получателя нет беседы) ──
|
||||
* A,B,C online → B уходит offline → A шлёт ПЕРВОЕ сообщение "first" → mailbox →
|
||||
* B2 переподключается (без беседы) → тянет mailbox → получает "first". */
|
||||
enum dm2_phase {
|
||||
S2_WAIT_CONN,
|
||||
S2_SETUP,
|
||||
S2_WAIT_BGP,
|
||||
S2_B_OFFLINE,
|
||||
S2_WAIT_B_OFFLINE,
|
||||
S2_SEND, /* A: dm_start + dm_send "first" → mailbox (B offline, первое) */
|
||||
S2_WAIT_MAIL, /* C: блоб в dm_mail */
|
||||
S2_RECONNECT, /* create B2 (та же БД B, но без беседы) */
|
||||
S2_WAIT_B2, /* B2: получил "first" */
|
||||
S2_DONE,
|
||||
};
|
||||
|
||||
struct dm2_ctx {
|
||||
struct UASYNC* ua;
|
||||
struct UTUN_INSTANCE* inst[3];
|
||||
enum dm2_phase phase;
|
||||
int result;
|
||||
int ticks;
|
||||
void* timer;
|
||||
};
|
||||
|
||||
static void dm2_tick(void* arg) {
|
||||
struct dm2_ctx* t = arg;
|
||||
t->timer = NULL;
|
||||
if (t->result) return;
|
||||
|
||||
struct UTUN_INSTANCE* A = t->inst[IDX_A];
|
||||
struct UTUN_INSTANCE* B = t->inst[IDX_B];
|
||||
struct UTUN_INSTANCE* C = t->inst[IDX_C];
|
||||
uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B], nc = g_sh.nid[IDX_C];
|
||||
|
||||
switch (t->phase) {
|
||||
case S2_WAIT_CONN:
|
||||
if (conn_up(A, nc) && conn_up(C, na) && conn_up(B, nc) && conn_up(C, nb))
|
||||
t->phase = S2_SETUP;
|
||||
break;
|
||||
case S2_SETUP:
|
||||
do_setup(A, IDX_A);
|
||||
do_setup(B, IDX_B);
|
||||
do_setup(C, IDX_C);
|
||||
t->phase = S2_WAIT_BGP;
|
||||
break;
|
||||
case S2_WAIT_BGP:
|
||||
if (bgp_has(A, nc) && bgp_has(A, nb) && bgp_has(C, na) && bgp_has(C, nb) && bgp_has(B, na))
|
||||
t->phase = S2_B_OFFLINE;
|
||||
break;
|
||||
case S2_B_OFFLINE:
|
||||
utun_instance_destroy(B);
|
||||
t->inst[IDX_B] = NULL;
|
||||
t->phase = S2_WAIT_B_OFFLINE;
|
||||
break;
|
||||
case S2_WAIT_B_OFFLINE:
|
||||
if (!bgp_has(A, nb)) t->phase = S2_SEND;
|
||||
break;
|
||||
case S2_SEND:
|
||||
if (dm_start(A, nb, g_sh.x_pub[IDX_B], g_sh.ed_pub[IDX_B], "B", CH_ID) != 0
|
||||
|| dm_send(A, g_sh.conv_id, "text", (const uint8_t*)"first", 5) != 0) {
|
||||
fprintf(stderr, "A: dm_start/send first failed\n");
|
||||
t->result = 2;
|
||||
} else {
|
||||
t->phase = S2_WAIT_MAIL;
|
||||
}
|
||||
break;
|
||||
case S2_WAIT_MAIL:
|
||||
if (dm_mail_count(C) > 0) t->phase = S2_RECONNECT;
|
||||
break;
|
||||
case S2_RECONNECT: {
|
||||
char cfg[512];
|
||||
snprintf(cfg, sizeof(cfg), "%s/b.conf", getenv("UTUN_TEST_DIR") ? getenv("UTUN_TEST_DIR") : ".");
|
||||
struct UTUN_INSTANCE* b2 = utun_instance_create(t->ua, cfg);
|
||||
if (!b2) { fprintf(stderr, "B2 create failed\n"); t->result = 2; break; }
|
||||
utun_instance_init(b2);
|
||||
t->inst[IDX_B] = b2;
|
||||
t->phase = S2_WAIT_B2;
|
||||
break;
|
||||
}
|
||||
case S2_WAIT_B2:
|
||||
if (dm_msg_has(B, "first") && dm_mail_count(C) == 0) t->phase = S2_DONE;
|
||||
break;
|
||||
case S2_DONE:
|
||||
t->result = 1;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t->phase == S2_DONE) { t->result = 1; uasync_stop(t->ua); return; }
|
||||
|
||||
if (++t->ticks > MAX_TICKS) {
|
||||
fprintf(stderr, "s2: timeout in phase %d (A_conn=%d C_conn=%d B_conn=%d)\n",
|
||||
(int)t->phase,
|
||||
conn_up(A, nc), conn_up(C, na), B ? conn_up(B, nc) : -1);
|
||||
t->result = 2;
|
||||
uasync_stop(t->ua);
|
||||
return;
|
||||
}
|
||||
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, dm2_tick, "dm2_tick");
|
||||
}
|
||||
|
||||
/* ── main ── */
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -447,6 +549,56 @@ int main(int argc, char** argv) {
|
||||
|
||||
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd);
|
||||
|
||||
/* сценарий 2: отдельная топология (свежие ключи/БД) */
|
||||
char dir2[512]; snprintf(dir2, sizeof(dir2), "/tmp/utun_dm_e2e2_XXXXXX");
|
||||
if (!mkdtemp(dir2)) { fprintf(stderr, "mkdtemp dir2 failed\n"); return 1; }
|
||||
setenv("UTUN_TEST_DIR", dir2, 1);
|
||||
fill_shared(70000 + (getpid() % 2000));
|
||||
write_configs(dir2);
|
||||
|
||||
TEST("first message offline: mailbox delivery without prior conversation"); {
|
||||
struct UASYNC* ua = uasync_create();
|
||||
if (!ua) { FAIL("uasync_create failed"); return 1; }
|
||||
|
||||
struct dm2_ctx t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.ua = ua;
|
||||
t.phase = S2_WAIT_CONN;
|
||||
|
||||
char cfgA[512], cfgB[512], cfgC[512];
|
||||
snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir2);
|
||||
snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir2);
|
||||
snprintf(cfgC, sizeof(cfgC), "%s/c.conf", dir2);
|
||||
|
||||
t.inst[IDX_A] = utun_instance_create(ua, cfgA);
|
||||
t.inst[IDX_B] = utun_instance_create(ua, cfgB);
|
||||
t.inst[IDX_C] = utun_instance_create(ua, cfgC);
|
||||
|
||||
if (!t.inst[IDX_A] || !t.inst[IDX_B] || !t.inst[IDX_C]) {
|
||||
FAIL("instance create failed A=%p B=%p C=%p",
|
||||
(void*)t.inst[IDX_A], (void*)t.inst[IDX_B], (void*)t.inst[IDX_C]);
|
||||
uasync_destroy(ua, 0);
|
||||
return 1;
|
||||
}
|
||||
utun_instance_init(t.inst[IDX_A]);
|
||||
utun_instance_init(t.inst[IDX_C]);
|
||||
utun_instance_init(t.inst[IDX_B]);
|
||||
|
||||
t.timer = uasync_set_timeout(ua, TICK_TB, &t, dm2_tick, "dm2_tick");
|
||||
uasync_mainloop(ua);
|
||||
|
||||
int ok = (t.result == 1);
|
||||
|
||||
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
|
||||
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
|
||||
if (t.inst[IDX_C]) utun_instance_destroy(t.inst[IDX_C]);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);
|
||||
}
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", dir2); (void)!system(cmd);
|
||||
|
||||
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
|
||||
return G_FAILED > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user