Delete local conversations atomically, retain replay protection and sequence counters, and guard asynchronous media work by history generation. Add Android avatar menu and confirmation, persistent file cleanup, and regression coverage. Fix unnamed member wire padding and compact Android member action spacing. Validation: 158 core tests passed; targeted DM tests, desktop build, Android unit tests and debug APK build passed.
215 lines
14 KiB
C
215 lines
14 KiB
C
/* Android bridge + настоящее ядро: ссылки, подтверждения, media workers и старая история.
|
|
* Подменяем только владельца instance, чтобы весь сценарий выполнялся в одном uasync-потоке. */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include "../lib/mem.h"
|
|
#include "../lib/debug_config.h"
|
|
#include "../src/utun_instance.h"
|
|
#include "../src/chat/chat_core.h"
|
|
#include "../src/chat/chat_event.h"
|
|
#include "../src/routing_layer/topo_node_sqlite.h"
|
|
#include "../src/transport_layer/etcp_connections.h"
|
|
#include "../src/config_parser.h"
|
|
#include "../tools/chatgui-android/jni_bridge/android_jni_bridge.h"
|
|
#include "test_utils.h"
|
|
|
|
#define REQUIRE(value) do { if (!(value)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android reply check failed line=%d", __LINE__); return 1; } } while (0)
|
|
static struct UTUN_INSTANCE* instance;
|
|
static struct UASYNC* async;
|
|
static int results[32];
|
|
struct UTUN_INSTANCE* __wrap_instance_lite_get_instance(void) { return instance; }
|
|
struct UASYNC* __wrap_instance_lite_get_uasync(void) { return async; }
|
|
|
|
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) {
|
|
(void)inst;
|
|
if (type != CHAT_EVT_MSG_SUBMITTED || length != 9) return;
|
|
uint64_t id = 0;
|
|
for (int i = 0; i < 8; i++) id |= (uint64_t)data[i] << (8 * i);
|
|
if (id < 32) results[id] = data[8] ? 1 : -1;
|
|
}
|
|
|
|
static int wait_result(int id) {
|
|
for (int i = 0; i < 500 && !results[id]; i++) uasync_poll(async, 20);
|
|
return results[id];
|
|
}
|
|
|
|
static int extract(const char* json, const char* path, char* value, size_t capacity) {
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(instance->topo_sqlite_db, "SELECT json_extract(?1,?2)", -1, &st, NULL) != SQLITE_OK) return -1;
|
|
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC);
|
|
int rc = sqlite3_step(st);
|
|
if (rc == SQLITE_ROW) snprintf(value, capacity, "%s", sqlite3_column_text(st, 0));
|
|
sqlite3_finalize(st);
|
|
return rc == SQLITE_ROW ? 0 : -1;
|
|
}
|
|
|
|
static int check_reply(const char* channel, const char* original, const char* type) {
|
|
char* json = utun_bridge_get_messages_json(channel, 1, NULL, 0);
|
|
char reply[65], content_type[64];
|
|
int ok = !extract(json, "$[0].replyTo", reply, sizeof(reply)) && !strcmp(reply, original) &&
|
|
!extract(json, "$[0].contentType", content_type, sizeof(content_type)) && !strcmp(content_type, type);
|
|
u_free(json); return ok;
|
|
}
|
|
|
|
/* JSON адресов для Android сохраняет IPv4/IPv6, протокол, порт, socket_id и тип профиля. */
|
|
static int check_addresses(const char *channel) {
|
|
struct TOPO_SOCKMETA4 meta4 = {.id = 5, .config_type = CFG_SERVER_TYPE_PUBLIC};
|
|
struct TOPO_SOCKMETA6 meta6 = {.id = 6, .config_type = CFG_SERVER_TYPE_PUBLIC,
|
|
.address_profile = {ADDRESS_MAP_UNCHANGED, ADDRESS_IN_OPEN, ADDRESS_SOURCE_ETCP}};
|
|
struct TOPO_ADDR4 addr4 = {.addr = {194,63,142,140}, .port = 1333, .socket_id = 5, .protocol = TOPO_PROTO_UDP};
|
|
struct TOPO_ADDR6 addr6 = {.addr = {0x2a,0x04,0x52,0,0,0,0,0,0,0,0,0,0,0,0,0xf},
|
|
.port = 443, .socket_id = 6, .protocol = TOPO_PROTO_TCP};
|
|
struct TOPO_NODE node = {.node_id = instance->node_id, .v4_sock_meta = &meta4, .v4_addrs = &addr4,
|
|
.v6_sock_meta = &meta6, .v6_addrs = &addr6};
|
|
REQUIRE(!topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, &node));
|
|
char *json = utun_bridge_get_member_detail_json(channel, instance->node_id);
|
|
char value[128];
|
|
REQUIRE(json && !extract(json, "$.adr[0].ad", value, sizeof(value)) && !strcmp(value, "194.63.142.140"));
|
|
REQUIRE(!extract(json, "$.adr[0].po", value, sizeof(value)) && !strcmp(value, "1333"));
|
|
REQUIRE(!extract(json, "$.adr[0].at", value, sizeof(value)) && !strcmp(value, "0")); /* адрес интерфейса, не NAT */
|
|
REQUIRE(!extract(json, "$.adr[0].pt", value, sizeof(value)) && !strcmp(value, "UDP"));
|
|
REQUIRE(!extract(json, "$.adr[0].si", value, sizeof(value)) && !strcmp(value, "5"));
|
|
REQUIRE(!extract(json, "$.adr[1].ad", value, sizeof(value)) && !strcmp(value, "2a04:5200::f"));
|
|
REQUIRE(!extract(json, "$.adr[1].at", value, sizeof(value)) && !strcmp(value, "1"));
|
|
REQUIRE(!extract(json, "$.adr[1].pt", value, sizeof(value)) && !strcmp(value, "TCP"));
|
|
REQUIRE(!extract(json, "$.adr[1].po", value, sizeof(value)) && !strcmp(value, "443"));
|
|
REQUIRE(!extract(json, "$.adr[1].si", value, sizeof(value)) && !strcmp(value, "6"));
|
|
REQUIRE(!extract(json, "$.adr[0].nmapping", value, sizeof(value)) && !strcmp(value, "0"));
|
|
REQUIRE(!extract(json, "$.adr[0].nsource", value, sizeof(value)) && !strcmp(value, "0"));
|
|
u_free(json);
|
|
/* addr_type=0 остаётся тем же, но измеренный UNCHANGED должен дойти до GUI отдельно. */
|
|
meta4.address_profile = (struct address_profile){ADDRESS_MAP_UNCHANGED, ADDRESS_IN_UNKNOWN, ADDRESS_SOURCE_ETCP};
|
|
REQUIRE(!topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, &node));
|
|
json = utun_bridge_get_member_detail_json(channel, instance->node_id);
|
|
REQUIRE(json && !extract(json, "$.adr[0].at", value, sizeof(value)) && !strcmp(value, "0"));
|
|
REQUIRE(!extract(json, "$.adr[0].nmapping", value, sizeof(value)) && !strcmp(value, "1"));
|
|
REQUIRE(!extract(json, "$.adr[0].ninbound", value, sizeof(value)) && !strcmp(value, "0"));
|
|
REQUIRE(!extract(json, "$.adr[0].nsource", value, sizeof(value)) && !strcmp(value, "1"));
|
|
u_free(json);
|
|
meta4.address_profile.mapping = ADDRESS_MAP_UNKNOWN;
|
|
addr4.type = TOPO_ADDR_NAT; meta4.address_profile.source = ADDRESS_SOURCE_ETCP;
|
|
REQUIRE(!topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, &node));
|
|
json = utun_bridge_get_member_detail_json(channel, instance->node_id);
|
|
REQUIRE(json && !extract(json, "$.adr[0].at", value, sizeof(value)) && !strcmp(value, "4")); /* NAT, тип неизвестен */
|
|
u_free(json);
|
|
meta4.address_profile.mapping = ADDRESS_MAP_EIM;
|
|
REQUIRE(!topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, &node));
|
|
json = utun_bridge_get_member_detail_json(channel, instance->node_id);
|
|
REQUIRE(json && !extract(json, "$.adr[0].at", value, sizeof(value)) && !strcmp(value, "6")); /* EIM? */
|
|
u_free(json); return 0;
|
|
}
|
|
|
|
/* Собственные сокеты передают профиль независимо от адреса интерфейса; тестовые порты скрыты. */
|
|
static int check_local_sockets(void) {
|
|
struct ETCP_SOCKET* saved = instance->etcp_sockets;
|
|
struct ETCP_SOCKET tcp = {.sock_id = 7, .is_tcp = 1, .name = "as_wlan0_v4_tcp",
|
|
.address_profile = {ADDRESS_MAP_UNKNOWN, ADDRESS_IN_UNKNOWN, ADDRESS_SOURCE_ETCP}};
|
|
struct ETCP_SOCKET udp = {.sock_id = 8, .name = "as_wlan0_v6_udp",
|
|
.address_profile = {ADDRESS_MAP_EIM, ADDRESS_IN_OPEN, ADDRESS_SOURCE_ETCP}};
|
|
struct ETCP_SOCKET probe = {.sock_id = 9, .probe_only = 1};
|
|
struct sockaddr_in* local4 = (struct sockaddr_in*)&tcp.interface_addr;
|
|
local4->sin_family = AF_INET; local4->sin_port = htons(9892); inet_pton(AF_INET, "192.168.40.250", &local4->sin_addr);
|
|
struct sockaddr_in* external4 = (struct sockaddr_in*)&tcp.nat_addr;
|
|
external4->sin_family = AF_INET; inet_pton(AF_INET, "194.63.142.140", &external4->sin_addr);
|
|
struct sockaddr_in6* local6 = (struct sockaddr_in6*)&udp.interface_addr;
|
|
local6->sin6_family = AF_INET6; local6->sin6_port = htons(9893); inet_pton(AF_INET6, "fd00::1", &local6->sin6_addr);
|
|
struct sockaddr_in6* external6 = (struct sockaddr_in6*)&udp.nat_addr;
|
|
external6->sin6_family = AF_INET6; external6->sin6_port = htons(4101); inet_pton(AF_INET6, "2a04:5200::f", &external6->sin6_addr);
|
|
tcp.next = &udp; udp.next = &probe; instance->etcp_sockets = &tcp;
|
|
char* json = utun_bridge_get_local_sockets_json();
|
|
instance->etcp_sockets = saved;
|
|
char value[128];
|
|
REQUIRE(json && !extract(json, "$[0].nmapping", value, sizeof(value)) && !strcmp(value, "0"));
|
|
REQUIRE(!extract(json, "$[0].nsource", value, sizeof(value)) && !strcmp(value, "1"));
|
|
REQUIRE(!extract(json, "$[0].ad", value, sizeof(value)) && !strcmp(value, "192.168.40.250"));
|
|
REQUIRE(!extract(json, "$[0].external", value, sizeof(value)) && !strcmp(value, "194.63.142.140"));
|
|
REQUIRE(!extract(json, "$[0].externalPort", value, sizeof(value)) && !strcmp(value, "0"));
|
|
REQUIRE(!extract(json, "$[1].nmapping", value, sizeof(value)) && !strcmp(value, "2"));
|
|
REQUIRE(!extract(json, "$[1].ninbound", value, sizeof(value)) && !strcmp(value, "1"));
|
|
REQUIRE(!extract(json, "$[1].external", value, sizeof(value)) && !strcmp(value, "2a04:5200::f"));
|
|
REQUIRE(!extract(json, "$[1].externalPort", value, sizeof(value)) && !strcmp(value, "4101"));
|
|
REQUIRE(!extract(json, "$[#-1].si", value, sizeof(value)) && !strcmp(value, "8"));
|
|
u_free(json); return 0;
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
|
char directory[512] = "/tmp/utun_android_replies_XXXXXX", config[1024], channel[64], hex[65];
|
|
REQUIRE(!test_mkdtemp(directory));
|
|
snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory);
|
|
async = uasync_create(); REQUIRE(async);
|
|
instance = utun_instance_create_from_str(async, config); REQUIRE(instance);
|
|
REQUIRE(!utun_core_start(instance) && !chat_service_start(instance));
|
|
uasync_mark_running(async);
|
|
chat_event_set_handler(instance, event);
|
|
chat_core_create_channel_auto(instance, "Android replies");
|
|
char* json = utun_bridge_get_channels_json();
|
|
REQUIRE(!extract(json, "$[0].id", channel, sizeof(channel))); u_free(json);
|
|
REQUIRE(!check_addresses(channel));
|
|
REQUIRE(!check_local_sockets());
|
|
uint8_t empty[32] = {0}, original[32], missing[32]; memset(missing, 0xff, sizeof(missing));
|
|
REQUIRE(!utun_bridge_send_message(channel, "Привет 😀 \"original\"\nnext line", empty, 1) && wait_result(1) == 1);
|
|
json = utun_bridge_get_messages_json(channel, 1, NULL, 0);
|
|
REQUIRE(!extract(json, "$[0].messageId", hex, sizeof(hex)) && strlen(hex) == 64);
|
|
REQUIRE(strstr(json, "Привет 😀")); u_free(json);
|
|
for (unsigned i = 0; i < 32; i++) { unsigned value; REQUIRE(sscanf(hex + 2 * i, "%2x", &value) == 1); original[i] = value; }
|
|
|
|
/* Оригинал выходит из последнего окна; одинаковые timestamp сортируются по подписи. */
|
|
for (int i = 0; i < 220; i++) {
|
|
char body[32]; snprintf(body, sizeof(body), "History %d", i);
|
|
struct chat_msg_submit req = {0}; req.inst = instance;
|
|
snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); strcpy(req.content_type, "text/plain");
|
|
req.data = (uint8_t*)body; req.data_len = strlen(body);
|
|
REQUIRE(!chat_core_submit_message(instance, &req));
|
|
}
|
|
json = utun_bridge_get_messages_json(channel, 100, NULL, 0); REQUIRE(!strstr(json, hex)); u_free(json);
|
|
json = utun_bridge_get_messages_json(channel, 100, original, 0); REQUIRE(strstr(json, hex));
|
|
char length[32];
|
|
sqlite3_stmt* st = NULL;
|
|
REQUIRE(sqlite3_prepare_v2(instance->topo_sqlite_db, "SELECT json_array_length(?)", -1, &st, NULL) == SQLITE_OK);
|
|
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); REQUIRE(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_int(st, 0) == 100);
|
|
sqlite3_finalize(st); u_free(json);
|
|
json = utun_bridge_get_messages_json(channel, 1, original, 1);
|
|
REQUIRE(strstr(json, "Привет 😀") && !extract(json, "$[0].replyTo", length, sizeof(length)) && !length[0]); u_free(json);
|
|
json = utun_bridge_get_messages_json(channel, 1, missing, 1); REQUIRE(!strcmp(json, "[]")); u_free(json);
|
|
REQUIRE(!utun_bridge_send_message(channel, "Reply to old message", original, 2) && wait_result(2) == 1);
|
|
REQUIRE(check_reply(channel, hex, "text/plain"));
|
|
REQUIRE(!utun_bridge_send_message(channel, "Missing original", missing, 3) && wait_result(3) == -1);
|
|
REQUIRE(check_reply(channel, hex, "text/plain"));
|
|
|
|
char file[1024]; snprintf(file, sizeof(file), "%s/source.bin", directory);
|
|
FILE* source = fopen(file, "wb"); REQUIRE(source);
|
|
REQUIRE(fwrite("Android attachment", 1, 18, source) == 18 && fclose(source) == 0);
|
|
REQUIRE(!utun_bridge_attachment_send(channel, file, directory, original, 4) && wait_result(4) == 1);
|
|
REQUIRE(check_reply(channel, hex, "application/octet-stream"));
|
|
REQUIRE(!utun_bridge_photo_send(channel, file, directory, 24, 24, original, 5) && wait_result(5) == 1);
|
|
REQUIRE(check_reply(channel, hex, "image/jpeg"));
|
|
/* Мост принимает уже подготовленный MP4: содержимое не декодируется повторно. */
|
|
REQUIRE(!utun_bridge_video_send(channel, file, directory, 1000, 24, 24, original, 6, 0) && wait_result(6) == 1);
|
|
REQUIRE(check_reply(channel, hex, "video/mp4"));
|
|
REQUIRE(access(file, F_OK) != 0);
|
|
|
|
REQUIRE(!utun_bridge_voice_start(channel, original, 7, 0));
|
|
int16_t pcm[960] = {0};
|
|
for (int i = 0; i < 30; i++) { REQUIRE(!utun_bridge_voice_feed(pcm, 960)); uasync_poll(async, 20); }
|
|
/* Poll может вернуться немедленно по событию; длительность измеряется CLOCK_MONOTONIC. */
|
|
struct timespec pause = {0, 650000000}; nanosleep(&pause, NULL);
|
|
int duration = 0;
|
|
REQUIRE(!utun_bridge_voice_stop(&duration) && duration >= 500 && wait_result(7) == 1);
|
|
REQUIRE(check_reply(channel, hex, "audio/opus"));
|
|
REQUIRE(!utun_bridge_voice_start(channel, original, 8, 0)); utun_bridge_voice_cancel();
|
|
uasync_poll(async, 0); REQUIRE(results[8] == 0);
|
|
REQUIRE(utun_bridge_voice_stop(&duration) < 0);
|
|
REQUIRE(!utun_bridge_attachment_send(channel, "/nonexistent/android-reply", directory, original, 9) && wait_result(9) == -1);
|
|
REQUIRE(check_reply(channel, hex, "audio/opus"));
|
|
|
|
/* Ссылка остаётся в JSON после перезапуска сервиса. */
|
|
chat_service_stop(instance); REQUIRE(!chat_service_start(instance));
|
|
REQUIRE(check_reply(channel, hex, "audio/opus"));
|
|
utun_bridge_destroy();
|
|
utun_instance_destroy(instance); uasync_destroy(async, 0);
|
|
printf("test_android_replies: text, media, voice, results, history and restart PASS\n");
|
|
return 0;
|
|
}
|