diff --git a/.cirrus.yml b/.cirrus.yml index 5c5e212770..9a3d27f35b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -92,6 +92,7 @@ bazel-tsan_task: -//c-toxcore/auto_tests:conference_av_test -//c-toxcore/auto_tests:conference_test -//c-toxcore/auto_tests:file_transfer_test + -//c-toxcore/auto_tests:group_tcp_test -//c-toxcore/auto_tests:onion_test -//c-toxcore/auto_tests:tcp_relay_test -//c-toxcore/auto_tests:tox_many_test diff --git a/.gitignore b/.gitignore index 9dcf09b692..fc3cc5f733 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ libtool .libs .dirstamp build/ +*.nvim* +*.vim* #kdevelop .kdev/ @@ -88,3 +90,5 @@ cscope.files # rpm tox.spec + +.idea/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 00ddfc7bf8..bf3d433d57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,11 @@ if(NOT USE_IPV6) add_definitions(-DUSE_IPV6=0) endif() +option(USE_TEST_NETWORK "Use a separate test network with different packet IDs" OFF) +if(USE_TEST_NETWORK) + add_definitions(-DUSE_TEST_NETWORK=1) +endif() + option(BUILD_MISC_TESTS "Build additional tests and utilities" OFF) option(BUILD_FUN_UTILS "Build additional just for fun utilities" OFF) @@ -241,11 +246,20 @@ set(toxcore_SOURCES toxcore/friend_requests.c toxcore/friend_requests.h toxcore/group.c + toxcore/group_chats.c + toxcore/group_chats.h + toxcore/group_common.h + toxcore/group_connection.c + toxcore/group_connection.h toxcore/group.h toxcore/group_announce.c toxcore/group_announce.h toxcore/group_moderation.c toxcore/group_moderation.h + toxcore/group_onion_announce.c + toxcore/group_onion_announce.h + toxcore/group_pack.c + toxcore/group_pack.h toxcore/LAN_discovery.c toxcore/LAN_discovery.h toxcore/list.c diff --git a/auto_tests/BUILD.bazel b/auto_tests/BUILD.bazel index 5dbf2fd301..babd3aca84 100644 --- a/auto_tests/BUILD.bazel +++ b/auto_tests/BUILD.bazel @@ -44,6 +44,7 @@ flaky_tests = { ":check_compat", "//c-toxcore/testing:misc_tools", "//c-toxcore/toxav", + "//c-toxcore/toxcore:Messenger", "//c-toxcore/toxcore:TCP_client", "//c-toxcore/toxcore:TCP_common", "//c-toxcore/toxcore:TCP_connection", diff --git a/auto_tests/CMakeLists.txt b/auto_tests/CMakeLists.txt index cf098f8c32..774e3e331a 100644 --- a/auto_tests/CMakeLists.txt +++ b/auto_tests/CMakeLists.txt @@ -36,6 +36,15 @@ auto_test(forwarding) auto_test(friend_connection) auto_test(friend_request) auto_test(friend_request_spam) +auto_test(group_general) +auto_test(group_invite) +auto_test(group_message) +auto_test(group_moderation) +auto_test(group_save) +auto_test(group_state) +auto_test(group_sync) +auto_test(group_tcp) +auto_test(group_topic) auto_test(invalid_tcp_proxy) auto_test(invalid_udp_proxy) auto_test(lan_discovery) diff --git a/auto_tests/Makefile.inc b/auto_tests/Makefile.inc index 59adf422c2..6b73c2c185 100644 --- a/auto_tests/Makefile.inc +++ b/auto_tests/Makefile.inc @@ -17,6 +17,7 @@ TESTS = \ forwarding_test \ friend_connection_test \ friend_request_test \ + group_state_test \ invalid_tcp_proxy_test \ invalid_udp_proxy_test \ lan_discovery_test \ @@ -126,6 +127,10 @@ friend_request_test_SOURCES = ../auto_tests/friend_request_test.c friend_request_test_CFLAGS = $(AUTOTEST_CFLAGS) friend_request_test_LDADD = $(AUTOTEST_LDADD) +group_state_test_SOURCES = ../auto_tests/group_state_test.c +group_state_test_CFLAGS = $(AUTOTEST_CFLAGS) +group_state_test_LDADD = $(AUTOTEST_LDADD) + invalid_tcp_proxy_test_SOURCES = ../auto_tests/invalid_tcp_proxy_test.c invalid_tcp_proxy_test_CFLAGS = $(AUTOTEST_CFLAGS) invalid_tcp_proxy_test_LDADD = $(AUTOTEST_LDADD) diff --git a/auto_tests/auto_test_support.c b/auto_tests/auto_test_support.c index bdc700e35b..cb409738ef 100644 --- a/auto_tests/auto_test_support.c +++ b/auto_tests/auto_test_support.c @@ -65,6 +65,13 @@ static const struct BootstrapNodes { 0x65, 0x4A, 0x37, 0x58, 0xC5, 0x3E, 0x02, 0x73, 0xEC, 0xFC, 0x4D, 0x12, 0xC2, 0x1D, 0xCA, 0x48, }, + { + "tox.plastiras.org", 38445, + 0x5E, 0x47, 0xBA, 0x1D, 0xC3, 0x91, 0x3E, 0xB2, + 0xCB, 0xF2, 0xD6, 0x4C, 0xE4, 0xF2, 0x3D, 0x8B, + 0xFE, 0x53, 0x91, 0xBF, 0xAB, 0xE5, 0xC4, 0x3C, + 0x5B, 0xAD, 0x13, 0xF0, 0xA4, 0x14, 0xCD, 0x77, + }, #endif // USE_TEST_NETWORK { nullptr, 0, 0 }, }; diff --git a/auto_tests/group_general_test.c b/auto_tests/group_general_test.c new file mode 100644 index 0000000000..1a506ab4f7 --- /dev/null +++ b/auto_tests/group_general_test.c @@ -0,0 +1,437 @@ +/* + * Tests that we can connect to a public group chat through the DHT and make basic queries + * about the group, other peers, and ourselves. We also make sure we can disconnect and + * reconnect to a group while retaining our credentials. + */ + +#include +#include +#include + +#include "auto_test_support.h" + +typedef struct State { + size_t peer_joined_count; + size_t self_joined_count; + size_t peer_exit_count; + bool peer_nick; + bool peer_status; + uint32_t peer_id; + bool is_founder; +} State; + +#define NUM_GROUP_TOXES 2 + +#define GROUP_NAME "NASA Headquarters" +#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) + +#define TOPIC "Funny topic here" +#define TOPIC_LEN (sizeof(TOPIC) - 1) + +#define PEER0_NICK "Lois" +#define PEER0_NICK_LEN (sizeof(PEER0_NICK) - 1) + +#define PEER0_NICK2 "Terry Davis" +#define PEER0_NICK2_LEN (sizeof(PEER0_NICK2) - 1) + +#define PEER1_NICK "Bran" +#define PEER1_NICK_LEN (sizeof(PEER1_NICK) - 1) + +#define EXIT_MESSAGE "Goodbye world" +#define EXIT_MESSAGE_LEN (sizeof(EXIT_MESSAGE) - 1) + +#define PEER_LIMIT 20 + +static bool all_group_peers_connected(AutoTox *autotoxes, uint32_t tox_count, uint32_t groupnumber, size_t name_length) +{ + for (size_t i = 0; i < tox_count; ++i) { + // make sure we got an invite response + if (tox_group_get_name_size(autotoxes[i].tox, groupnumber, nullptr) != name_length) { + return false; + } + + // make sure we got a sync response + if (tox_group_get_peer_limit(autotoxes[i].tox, groupnumber, nullptr) != PEER_LIMIT) { + return false; + } + + // make sure we're actually connected + if (!tox_group_is_connected(autotoxes[i].tox, groupnumber, nullptr)) { + return false; + } + } + + return true; +} + +static void group_peer_join_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + // we do a connection test here for fun + Tox_Err_Group_Peer_Query pq_err; + TOX_CONNECTION connection_status = tox_group_peer_get_connection_status(tox, groupnumber, peer_id, &pq_err); + ck_assert(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(connection_status != TOX_CONNECTION_NONE); + + Tox_Group_Role role = tox_group_peer_get_role(tox, groupnumber, peer_id, &pq_err); + ck_assert_msg(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK, "%d", pq_err); + + Tox_User_Status status = tox_group_peer_get_status(tox, groupnumber, peer_id, &pq_err); + ck_assert_msg(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK, "%d", pq_err); + + size_t peer_name_len = tox_group_peer_get_name_size(tox, groupnumber, peer_id, &pq_err); + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + + ck_assert(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + tox_group_peer_get_name(tox, groupnumber, peer_id, (uint8_t *) peer_name, &pq_err); + ck_assert(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK); + + peer_name[peer_name_len] = 0; + + // make sure we see the correct peer state on join + if (!state->is_founder) { + ck_assert_msg(role == TOX_GROUP_ROLE_FOUNDER, "wrong role: %d", role); + + if (state->peer_joined_count == 0) { + ck_assert_msg(status == TOX_USER_STATUS_NONE, "wrong status: %d", status); + ck_assert_msg(peer_name_len == PEER0_NICK_LEN, "wrong nick: %s", peer_name); + ck_assert(memcmp(peer_name, PEER0_NICK, peer_name_len) == 0); + } else { + ck_assert_msg(status == TOX_USER_STATUS_BUSY, "wrong status: %d", status); + ck_assert(peer_name_len == PEER0_NICK2_LEN); + ck_assert(memcmp(peer_name, PEER0_NICK2, peer_name_len) == 0); + } + } else { + ck_assert_msg(role == TOX_GROUP_ROLE_USER, "wrong role: %d", role); + ck_assert(peer_name_len == PEER1_NICK_LEN); + ck_assert(memcmp(peer_name, PEER1_NICK, peer_name_len) == 0); + + if (state->peer_joined_count == 0) { + ck_assert_msg(status == TOX_USER_STATUS_NONE, "wrong status: %d", status); + } else { + ck_assert_msg(status == TOX_USER_STATUS_AWAY, "wrong status: %d", status); + } + } + + state->peer_id = peer_id; + ++state->peer_joined_count; +} + +static void group_peer_self_join_handler(Tox *tox, uint32_t groupnumber, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + // make sure we see our own correct peer state on join callback + + Tox_Err_Group_Self_Query sq_err; + size_t self_length = tox_group_self_get_name_size(tox, groupnumber, &sq_err); + + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + uint8_t self_name[TOX_MAX_NAME_LENGTH]; + tox_group_self_get_name(tox, groupnumber, self_name, &sq_err); + + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + TOX_USER_STATUS self_status = tox_group_self_get_status(tox, groupnumber, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + Tox_Group_Role self_role = tox_group_self_get_role(tox, groupnumber, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (state->is_founder) { + // founder doesn't get a self join callback on initial creation of group + ck_assert(self_length == PEER0_NICK2_LEN); + ck_assert(memcmp(self_name, PEER0_NICK2, self_length) == 0); + ck_assert(self_status == TOX_USER_STATUS_BUSY); + ck_assert(self_role == TOX_GROUP_ROLE_FOUNDER); + } else { + ck_assert(self_length == PEER1_NICK_LEN); + ck_assert(memcmp(self_name, PEER1_NICK, self_length) == 0); + ck_assert(self_role == TOX_GROUP_ROLE_USER); + ck_assert(self_status == TOX_USER_STATUS_NONE); + } + + // make sure we see correct group state on join callback + uint8_t group_name[GROUP_NAME_LEN]; + uint8_t topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + + ck_assert(tox_group_get_peer_limit(tox, groupnumber, nullptr) == PEER_LIMIT); + ck_assert(tox_group_get_name_size(tox, groupnumber, nullptr) == GROUP_NAME_LEN); + ck_assert(tox_group_get_topic_size(tox, groupnumber, nullptr) == TOPIC_LEN); + + Tox_Err_Group_State_Queries query_err; + tox_group_get_name(tox, groupnumber, group_name, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "%d", query_err); + ck_assert(memcmp(group_name, GROUP_NAME, GROUP_NAME_LEN) == 0); + + tox_group_get_topic(tox, groupnumber, topic, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "%d", query_err); + ck_assert(memcmp(topic, TOPIC, TOPIC_LEN) == 0); + + ++state->self_joined_count; +} + +static void group_peer_exit_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *name, size_t name_length, const uint8_t *part_message, + size_t length, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ++state->peer_exit_count; + + // first exit is a disconnect. second is a real exit with a part message + if (state->peer_exit_count == 2) { + ck_assert(length == EXIT_MESSAGE_LEN); + ck_assert(memcmp(part_message, EXIT_MESSAGE, length) == 0); + } +} + +static void group_peer_name_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, const uint8_t *name, + size_t length, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + // note: we already test the name_get api call elsewhere + + ck_assert(length == PEER0_NICK2_LEN); + ck_assert(memcmp(name, PEER0_NICK2, length) == 0); + + state->peer_nick = true; +} + +static void group_peer_status_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_USER_STATUS status, + void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + Tox_Err_Group_Peer_Query err; + TOX_USER_STATUS cur_status = tox_group_peer_get_status(tox, groupnumber, peer_id, &err); + + ck_assert_msg(cur_status == status, "%d, %d", cur_status, status); + ck_assert(status == TOX_USER_STATUS_BUSY); + + state->peer_status = true; +} + +static void group_announce_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES == 2, "NUM_GROUP_TOXES needs to be 2"); + + Tox *tox0 = autotoxes[0].tox; + Tox *tox1 = autotoxes[1].tox; + State *state0 = (State *)autotoxes[0].state; + State *state1 = (State *)autotoxes[1].state; + + tox_callback_group_peer_join(tox0, group_peer_join_handler); + tox_callback_group_peer_join(tox1, group_peer_join_handler); + tox_callback_group_self_join(tox0, group_peer_self_join_handler); + tox_callback_group_self_join(tox1, group_peer_self_join_handler); + tox_callback_group_peer_name(tox1, group_peer_name_handler); + tox_callback_group_peer_status(tox1, group_peer_status_handler); + tox_callback_group_peer_exit(tox1, group_peer_exit_handler); + + // tox0 makes new group. + Tox_Err_Group_New err_new; + uint32_t groupnumber = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *) GROUP_NAME, + GROUP_NAME_LEN, (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, + &err_new); + ck_assert(err_new == TOX_ERR_GROUP_NEW_OK); + + state0->is_founder = true; + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + // changes the state (for sync check purposes) + Tox_Err_Group_Founder_Set_Peer_Limit limit_set_err; + tox_group_founder_set_peer_limit(tox0, groupnumber, PEER_LIMIT, &limit_set_err); + ck_assert_msg(limit_set_err == TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, "failed to set peer limit: %d", limit_set_err); + + Tox_Err_Group_Topic_Set tp_err; + tox_group_set_topic(tox0, groupnumber, (const uint8_t *)TOPIC, TOPIC_LEN, &tp_err); + ck_assert(tp_err == TOX_ERR_GROUP_TOPIC_SET_OK); + + // get the chat id of the new group. + Tox_Err_Group_State_Queries err_id; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + tox_group_get_chat_id(tox0, groupnumber, chat_id, &err_id); + ck_assert(err_id == TOX_ERR_GROUP_STATE_QUERIES_OK); + + // tox1 joins it. + Tox_Err_Group_Join err_join; + tox_group_join(tox1, chat_id, (const uint8_t *)PEER1_NICK, PEER1_NICK_LEN, nullptr, 0, &err_join); + ck_assert(err_join == TOX_ERR_GROUP_JOIN_OK); + + // peers see each other and themselves join + while (!state1->peer_joined_count || !state1->self_joined_count || !state0->peer_joined_count) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + // wait for group syncing to finish + while (!all_group_peers_connected(autotoxes, NUM_GROUP_TOXES, groupnumber, GROUP_NAME_LEN)) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + fprintf(stderr, "Peers connected to group\n"); + + // tox 0 changes name + Tox_Err_Group_Self_Name_Set n_err; + tox_group_self_set_name(tox0, groupnumber, (const uint8_t *)PEER0_NICK2, PEER0_NICK2_LEN, &n_err); + ck_assert(n_err == TOX_ERR_GROUP_SELF_NAME_SET_OK); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + Tox_Err_Group_Self_Query sq_err; + size_t self_length = tox_group_self_get_name_size(tox0, groupnumber, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_length == PEER0_NICK2_LEN); + + uint8_t self_name[TOX_MAX_NAME_LENGTH]; + tox_group_self_get_name(tox0, groupnumber, self_name, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(self_name, PEER0_NICK2, self_length) == 0); + + fprintf(stderr, "Peer 0 successfully changed nick\n"); + + // tox 0 changes status + Tox_Err_Group_Self_Status_Set s_err; + tox_group_self_set_status(tox0, groupnumber, TOX_USER_STATUS_BUSY, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_STATUS_SET_OK); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + TOX_USER_STATUS self_status = tox_group_self_get_status(tox0, groupnumber, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_status == TOX_USER_STATUS_BUSY); + + fprintf(stderr, "Peer 0 successfully changed status to %d\n", self_status); + + while (!state1->peer_nick && !state1->peer_status) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + // tox 0 and tox 1 should see the same public key for tox 0 + uint8_t tox0_self_pk[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + tox_group_self_get_public_key(tox0, groupnumber, tox0_self_pk, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + Tox_Err_Group_Peer_Query pq_err; + uint8_t tox0_pk_query[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + tox_group_peer_get_public_key(tox1, groupnumber, state1->peer_id, tox0_pk_query, &pq_err); + ck_assert(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(memcmp(tox0_pk_query, tox0_self_pk, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) == 0); + + fprintf(stderr, "Peer 0 disconnecting...\n"); + // tox 0 disconnects then reconnects + Tox_Err_Group_Disconnect d_err; + tox_group_disconnect(tox0, groupnumber, &d_err); + ck_assert(d_err == TOX_ERR_GROUP_DISCONNECT_OK); + + while (state1->peer_exit_count != 1) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + // tox 1 changes status while alone in the group + tox_group_self_set_status(tox1, groupnumber, TOX_USER_STATUS_AWAY, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_STATUS_SET_OK); + + fprintf(stderr, "Peer 0 reconnecting...\n"); + Tox_Err_Group_Reconnect r_err; + tox_group_reconnect(tox0, groupnumber, &r_err); + ck_assert(r_err == TOX_ERR_GROUP_RECONNECT_OK); + + while (state1->peer_joined_count != 2 && state0->self_joined_count == 2) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + for (size_t i = 0; i < 100; ++i) { // if we don't do this the exit packet never arrives + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + while (!all_group_peers_connected(autotoxes, NUM_GROUP_TOXES, groupnumber, GROUP_NAME_LEN)) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + // tox 0 should have the same public key and still be founder + uint8_t tox0_self_pk2[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + tox_group_self_get_public_key(tox0, groupnumber, tox0_self_pk2, &sq_err); + + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(tox0_self_pk2, tox0_self_pk, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) == 0); + + Tox_Group_Role self_role = tox_group_self_get_role(tox0, groupnumber, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + Tox_Group_Role other_role = tox_group_peer_get_role(tox1, groupnumber, state1->peer_id, &pq_err); + ck_assert(pq_err == TOX_ERR_GROUP_PEER_QUERY_OK); + + ck_assert(self_role == other_role && self_role == TOX_GROUP_ROLE_FOUNDER); + + uint32_t num_groups1 = tox_group_get_number_groups(tox0); + uint32_t num_groups2 = tox_group_get_number_groups(tox1); + + ck_assert(num_groups1 == num_groups2 && num_groups2 == 1); + + fprintf(stderr, "Both peers exiting group...\n"); + + Tox_Err_Group_Leave err_exit; + tox_group_leave(tox0, groupnumber, (const uint8_t *)EXIT_MESSAGE, EXIT_MESSAGE_LEN, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + + while (state1->peer_exit_count != 2) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + tox_group_leave(tox1, groupnumber, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + + num_groups1 = tox_group_get_number_groups(tox0); + num_groups2 = tox_group_get_number_groups(tox1); + + ck_assert(num_groups1 == num_groups2 && num_groups2 == 0); + + printf("All tests passed!\n"); +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options options = default_run_auto_options(); + options.graph = GRAPH_LINEAR; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_announce_test, sizeof(State), &options); + + return 0; +} + +#undef NUM_GROUP_TOXES +#undef PEER1_NICK +#undef PEER0_NICK +#undef PEER0_NICK_LEN +#undef PEER1_NICK_LEN +#undef GROUP_NAME +#undef GROUP_NAME_LEN +#undef PEER_LIMIT +#undef TOPIC +#undef TOPIC_LEN diff --git a/auto_tests/group_invite_test.c b/auto_tests/group_invite_test.c new file mode 100644 index 0000000000..f8dde62ab1 --- /dev/null +++ b/auto_tests/group_invite_test.c @@ -0,0 +1,283 @@ +/* + * Tests group invites as well as join restrictions, including password protection, privacy state, + * and peer limits. Ensures sure that the peer being blocked from joining successfully receives + * the invite fail packet with the correct message. + * + * This test also checks that many peers can successfully join the group simultaneously. + */ + +#include +#include +#include + +#include "auto_test_support.h" +#include "check_compat.h" + +typedef struct State { + uint32_t num_peers; + bool peer_limit_fail; + bool password_fail; + bool connected; + size_t messages_received; +} State; + +#define NUM_GROUP_TOXES 8 // must be > 7 + +#define PASSWORD "dadada" +#define PASS_LEN (sizeof(PASSWORD) - 1) + +#define WRONG_PASS "dadadada" +#define WRONG_PASS_LEN (sizeof(WRONG_PASS) - 1) + +static bool group_has_full_graph(const AutoTox *autotoxes, uint32_t group_number, uint32_t expected_peer_count) +{ + for (size_t i = 7; i < NUM_GROUP_TOXES; ++i) { + const State *state = (const State *)autotoxes[i].state; + + if (state->num_peers < expected_peer_count) { + return false; + } + } + + const State *state0 = (const State *)autotoxes[0].state; + const State *state1 = (const State *)autotoxes[1].state; + const State *state5 = (const State *)autotoxes[5].state; + + if (state0->num_peers < expected_peer_count || state1->num_peers < expected_peer_count + || state5->num_peers < expected_peer_count) { + return false; + } + + return true; +} + +static void group_join_fail_handler(Tox *tox, uint32_t group_number, Tox_Group_Join_Fail fail_type, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + switch (fail_type) { + case TOX_GROUP_JOIN_FAIL_PEER_LIMIT: { + state->peer_limit_fail = true; + break; + } + + case TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD: { + state->password_fail = true; + break; + } + + case TOX_GROUP_JOIN_FAIL_UNKNOWN: + + // intentional fallthrough + default: { + ck_assert_msg(false, "Got unknown join fail"); + return; + } + } +} + +static void group_self_join_handler(Tox *tox, uint32_t group_number, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + state->connected = true; +} + +static void group_peer_join_handler(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ++state->num_peers; + ck_assert(state->num_peers < NUM_GROUP_TOXES); +} + +static void group_invite_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES > 7, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + tox_callback_group_join_fail(autotoxes[i].tox, group_join_fail_handler); + tox_callback_group_self_join(autotoxes[i].tox, group_self_join_handler); + } + + Tox *tox0 = autotoxes[0].tox; + Tox *tox1 = autotoxes[1].tox; + Tox *tox2 = autotoxes[2].tox; + Tox *tox3 = autotoxes[3].tox; + Tox *tox4 = autotoxes[4].tox; + Tox *tox5 = autotoxes[5].tox; + Tox *tox6 = autotoxes[6].tox; + + State *state0 = (State *)autotoxes[0].state; + State *state2 = (State *)autotoxes[2].state; + State *state3 = (State *)autotoxes[3].state; + State *state4 = (State *)autotoxes[4].state; + State *state5 = (State *)autotoxes[5].state; + State *state6 = (State *)autotoxes[6].state; + + Tox_Err_Group_New new_err; + uint32_t groupnumber = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)"test", 4, + (const uint8_t *)"test", 4, &new_err); + ck_assert_msg(new_err == TOX_ERR_GROUP_NEW_OK, "tox_group_new failed: %d", new_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + + tox_group_get_chat_id(tox0, groupnumber, chat_id, &id_err); + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "%d", id_err); + + // peer 1 joins public group with no password + Tox_Err_Group_Join join_err; + tox_group_join(tox1, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + while (state0->num_peers < 1) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Peer 1 joined group\n"); + + // founder sets a password + Tox_Err_Group_Founder_Set_Password pass_set_err; + tox_group_founder_set_password(tox0, groupnumber, (const uint8_t *)PASSWORD, PASS_LEN, &pass_set_err); + ck_assert_msg(pass_set_err == TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, "%d", pass_set_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, 5000); + + // peer 2 attempts to join with no password + tox_group_join(tox2, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + while (!state2->password_fail) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Peer 2 successfully blocked with no password\n"); + + // peer 3 attempts to join with invalid password + tox_group_join(tox3, chat_id, (const uint8_t *)"Test", 4, (const uint8_t *)WRONG_PASS, WRONG_PASS_LEN, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + while (!state3->password_fail) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Peer 3 successfully blocked with invalid password\n"); + + // founder sets peer limit to 1 + Tox_Err_Group_Founder_Set_Peer_Limit limit_set_err; + tox_group_founder_set_peer_limit(tox0, groupnumber, 1, &limit_set_err); + ck_assert_msg(limit_set_err == TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, "%d", limit_set_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, 5000); + + // peer 4 attempts to join with correct password + tox_group_join(tox4, chat_id, (const uint8_t *)"Test", 4, (const uint8_t *)PASSWORD, PASS_LEN, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + while (!state4->peer_limit_fail) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Peer 4 successfully blocked from joining full group\n"); + + // founder removes password and increases peer limit to 100 + tox_group_founder_set_password(tox0, groupnumber, nullptr, 0, &pass_set_err); + ck_assert_msg(pass_set_err == TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, "%d", pass_set_err); + + tox_group_founder_set_peer_limit(tox0, groupnumber, 100, &limit_set_err); + ck_assert_msg(limit_set_err == TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, "%d", limit_set_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, 5000); + + // peer 5 attempts to join group + tox_group_join(tox5, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + while (!state5->connected) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Peer 5 successfully joined the group\n"); + + // founder makes group private + Tox_Err_Group_Founder_Set_Privacy_State priv_err; + tox_group_founder_set_privacy_state(tox0, groupnumber, TOX_GROUP_PRIVACY_STATE_PRIVATE, &priv_err); + ck_assert_msg(priv_err == TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, "%d", priv_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, 5000); + + // peer 6 attempts to join group via chat ID + tox_group_join(tox6, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + + // since we don't receive a fail packet in this case we just wait a while and check if we're in the group + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, 20000); + + ck_assert(!state6->connected); + + printf("Peer 6 failed to join private group via chat ID\n"); + + // founder makes group public again + tox_group_founder_set_privacy_state(tox0, groupnumber, TOX_GROUP_PRIVACY_STATE_PUBLIC, &priv_err); + ck_assert_msg(priv_err == TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, "%d", priv_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + const uint32_t num_new_peers = NUM_GROUP_TOXES - 7; + printf("Connecting %u peers at the same time\n", num_new_peers); + + for (size_t i = 7; i < NUM_GROUP_TOXES; ++i) { + tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + } + + const uint32_t expected_peer_count = num_new_peers + state0->num_peers + 1; + + while (!group_has_full_graph(autotoxes, groupnumber, expected_peer_count)) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("Every peer sees every other peer\n"); + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[i].tox, 0, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + } + + printf("All tests passed!\n"); + +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_invite_test, sizeof(State), &autotest_opts); + + return 0; +} + +#undef NUM_GROUP_TOXES +#undef PASSWORD +#undef PASS_LEN +#undef WRONG_PASS +#undef WRONG_PASS_LEN diff --git a/auto_tests/group_message_test.c b/auto_tests/group_message_test.c new file mode 100644 index 0000000000..d7a00a9fee --- /dev/null +++ b/auto_tests/group_message_test.c @@ -0,0 +1,546 @@ +/* + * Tests message sending capabilities, including: + * - The ability to send/receive plain, action, and custom messages + * - The lossless UDP implementation + * - The packet splitting implementation + * - The ignore feature + */ + +#include +#include +#include + +#include "auto_test_support.h" +#include "check_compat.h" +#include "../toxcore/util.h" + +typedef struct State { + uint32_t peer_id; + bool peer_joined; + bool message_sent; + bool message_received; + uint32_t pseudo_msg_id; + bool private_message_received; + size_t custom_packets_received; + size_t custom_private_packets_received; + bool lossless_check; + bool wraparound_check; + int32_t last_msg_recv; +} State; + +#define NUM_GROUP_TOXES 2 +#define MAX_NUM_MESSAGES_LOSSLESS_TEST 300 +#define MAX_NUM_MESSAGES_WRAPAROUND_TEST 9001 + +#define TEST_MESSAGE "Where is it I've read that someone condemned to death says or thinks, an hour before his death, that if he had to live on some high rock, on such a narrow ledge that he'd only room to stand, and the ocean, everlasting darkness, everlasting solitude, everlasting tempest around him, if he had to remain standing on a square yard of space all his life, a thousand years, eternity, it were better to live so than to die at once. Only to live, to live and live! Life, whatever it may be!" +#define TEST_MESSAGE_LEN (sizeof(TEST_MESSAGE) - 1) + +#define TEST_GROUP_NAME "Utah Data Center" +#define TEST_GROUP_NAME_LEN (sizeof(TEST_GROUP_NAME) - 1) + +#define TEST_PRIVATE_MESSAGE "Don't spill yer beans" +#define TEST_PRIVATE_MESSAGE_LEN (sizeof(TEST_PRIVATE_MESSAGE) - 1) + +#define TEST_CUSTOM_PACKET "Why'd ya spill yer beans?" +#define TEST_CUSTOM_PACKET_LEN (sizeof(TEST_CUSTOM_PACKET) - 1) + +#define TEST_CUSTOM_PRIVATE_PACKET "This is a custom private packet. Enjoy." +#define TEST_CUSTOM_PRIVATE_PACKET_LEN (sizeof(TEST_CUSTOM_PRIVATE_PACKET) - 1) + +#define IGNORE_MESSAGE "Am I bothering you?" +#define IGNORE_MESSAGE_LEN (sizeof(IGNORE_MESSAGE) - 1) + +#define PEER0_NICK "Thomas" +#define PEER0_NICK_LEN (sizeof(PEER0_NICK) - 1) + +#define PEER1_NICK "Winslow" +#define PEER1_NICK_LEN (sizeof(PEER1_NICK) - 1) + +static uint16_t get_message_checksum(const uint8_t *message, uint16_t length) +{ + uint16_t sum = 0; + + for (size_t i = 0; i < length; ++i) { + sum += message[i]; + } + + return sum; +} + +static void group_invite_handler(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *user_data) +{ + printf("invite arrived; accepting\n"); + Tox_Err_Group_Invite_Accept err_accept; + tox_group_invite_accept(tox, friend_number, invite_data, length, (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, + nullptr, 0, &err_accept); + ck_assert(err_accept == TOX_ERR_GROUP_INVITE_ACCEPT_OK); +} + +static void group_join_fail_handler(Tox *tox, uint32_t groupnumber, Tox_Group_Join_Fail fail_type, void *user_data) +{ + printf("join failed: %d\n", fail_type); +} + +static void group_peer_join_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert_msg(state->peer_joined == false, "Peer timedout"); + + printf("peer %u joined, sending message\n", peer_id); + state->peer_joined = true; + state->peer_id = peer_id; +} + +static void group_custom_private_packet_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data) +{ + ck_assert_msg(length == TEST_CUSTOM_PRIVATE_PACKET_LEN, + "Failed to receive custom private packet. Invalid length: %zu\n", length); + + char message_buf[TOX_MAX_CUSTOM_PACKET_SIZE + 1]; + memcpy(message_buf, data, length); + message_buf[length] = 0; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, groupnumber, peer_id, &q_err); + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_peer_get_name(tox, groupnumber, peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(memcmp(peer_name, PEER0_NICK, peer_name_len) == 0); + + Tox_Err_Group_Self_Query s_err; + size_t self_name_len = tox_group_self_get_name_size(tox, groupnumber, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_name_len <= TOX_MAX_NAME_LENGTH); + + char self_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_self_get_name(tox, groupnumber, (uint8_t *) self_name, &s_err); + self_name[self_name_len] = 0; + + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(self_name, PEER1_NICK, self_name_len) == 0); + + printf("%s sent custom private packet to %s: %s\n", peer_name, self_name, message_buf); + ck_assert(memcmp(message_buf, TEST_CUSTOM_PRIVATE_PACKET, length) == 0); + + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ++state->custom_private_packets_received; +} + +static void group_custom_packet_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data) +{ + ck_assert_msg(length == TEST_CUSTOM_PACKET_LEN, "Failed to receive custom packet. Invalid length: %zu\n", length); + + char message_buf[TOX_MAX_CUSTOM_PACKET_SIZE + 1]; + memcpy(message_buf, data, length); + message_buf[length] = 0; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, groupnumber, peer_id, &q_err); + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_peer_get_name(tox, groupnumber, peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(memcmp(peer_name, PEER0_NICK, peer_name_len) == 0); + + Tox_Err_Group_Self_Query s_err; + size_t self_name_len = tox_group_self_get_name_size(tox, groupnumber, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_name_len <= TOX_MAX_NAME_LENGTH); + + char self_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_self_get_name(tox, groupnumber, (uint8_t *) self_name, &s_err); + self_name[self_name_len] = 0; + + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(self_name, PEER1_NICK, self_name_len) == 0); + + printf("%s sent custom packet to %s: %s\n", peer_name, self_name, message_buf); + ck_assert(memcmp(message_buf, TEST_CUSTOM_PACKET, length) == 0); + + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ++state->custom_packets_received; +} + +static void group_message_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, uint32_t pseudo_msg_id, void *user_data) +{ + ck_assert(!(length == IGNORE_MESSAGE_LEN && memcmp(message, IGNORE_MESSAGE, length) == 0)); + ck_assert_msg(length == TEST_MESSAGE_LEN, "Failed to receive message. Invalid length: %zu\n", length); + + char message_buf[TOX_GROUP_MAX_MESSAGE_LENGTH + 1]; + memcpy(message_buf, message, length); + message_buf[length] = 0; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, groupnumber, peer_id, &q_err); + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_peer_get_name(tox, groupnumber, peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(memcmp(peer_name, PEER0_NICK, peer_name_len) == 0); + + Tox_Err_Group_Self_Query s_err; + size_t self_name_len = tox_group_self_get_name_size(tox, groupnumber, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_name_len <= TOX_MAX_NAME_LENGTH); + + char self_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_self_get_name(tox, groupnumber, (uint8_t *) self_name, &s_err); + self_name[self_name_len] = 0; + + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(self_name, PEER1_NICK, self_name_len) == 0); + + printf("%s sent message to %s:(id:%u) %s\n", peer_name, self_name, pseudo_msg_id, message_buf); + ck_assert(memcmp(message_buf, TEST_MESSAGE, length) == 0); + + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + state->message_received = true; + + state->pseudo_msg_id = pseudo_msg_id; +} + +static void group_private_message_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, void *user_data) +{ + ck_assert_msg(length == TEST_PRIVATE_MESSAGE_LEN, "Failed to receive message. Invalid length: %zu\n", length); + + char message_buf[TOX_GROUP_MAX_MESSAGE_LENGTH + 1]; + memcpy(message_buf, message, length); + message_buf[length] = 0; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, groupnumber, peer_id, &q_err); + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_peer_get_name(tox, groupnumber, peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(memcmp(peer_name, PEER0_NICK, peer_name_len) == 0); + + Tox_Err_Group_Self_Query s_err; + size_t self_name_len = tox_group_self_get_name_size(tox, groupnumber, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_name_len <= TOX_MAX_NAME_LENGTH); + + char self_name[TOX_MAX_NAME_LENGTH + 1]; + tox_group_self_get_name(tox, groupnumber, (uint8_t *) self_name, &s_err); + self_name[self_name_len] = 0; + + ck_assert(s_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(memcmp(self_name, PEER1_NICK, self_name_len) == 0); + + printf("%s sent private action to %s: %s\n", peer_name, self_name, message_buf); + ck_assert(memcmp(message_buf, TEST_PRIVATE_MESSAGE, length) == 0); + + ck_assert(type == TOX_MESSAGE_TYPE_ACTION); + + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + state->private_message_received = true; +} + +static void group_message_handler_lossless_test(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, uint32_t pseudo_msg_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(length >= 4 && length <= TOX_GROUP_MAX_MESSAGE_LENGTH); + + uint16_t start; + uint16_t checksum; + memcpy(&start, message, sizeof(uint16_t)); + memcpy(&checksum, message + sizeof(uint16_t), sizeof(uint16_t)); + + ck_assert_msg(start == state->last_msg_recv + 1, "Expected %d, got start %u", state->last_msg_recv + 1, start); + ck_assert_msg(checksum == get_message_checksum(message + 4, length - 4), "Wrong checksum"); + + state->last_msg_recv = start; + + if (state->last_msg_recv == MAX_NUM_MESSAGES_LOSSLESS_TEST) { + state->lossless_check = true; + } +} +static void group_message_handler_wraparound_test(Tox *tox, uint32_t groupnumber, uint32_t peer_id, + TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, uint32_t pseudo_msg_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(length == 2); + + uint16_t num; + memcpy(&num, message, sizeof(uint16_t)); + + ck_assert_msg(num == state->last_msg_recv + 1, "Expected %d, got start %u", state->last_msg_recv + 1, num); + + state->last_msg_recv = num; + + if (state->last_msg_recv == MAX_NUM_MESSAGES_WRAPAROUND_TEST) { + state->wraparound_check = true; + } +} + +static void group_message_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES >= 2, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + + const Random *rng = system_random(); + ck_assert(rng != nullptr); + + Tox *tox0 = autotoxes[0].tox; + Tox *tox1 = autotoxes[1].tox; + + State *state0 = (State *)autotoxes[0].state; + State *state1 = (State *)autotoxes[1].state; + + // initialize to different values + state0->pseudo_msg_id = 0; + state1->pseudo_msg_id = 1; + + tox_callback_group_invite(tox1, group_invite_handler); + tox_callback_group_join_fail(tox1, group_join_fail_handler); + tox_callback_group_peer_join(tox1, group_peer_join_handler); + tox_callback_group_join_fail(tox0, group_join_fail_handler); + tox_callback_group_peer_join(tox0, group_peer_join_handler); + tox_callback_group_message(tox0, group_message_handler); + tox_callback_group_custom_packet(tox0, group_custom_packet_handler); + tox_callback_group_custom_private_packet(tox0, group_custom_private_packet_handler); + tox_callback_group_private_message(tox0, group_private_message_handler); + + Tox_Err_Group_Send_Message err_send; + + fprintf(stderr, "Tox 0 creates new group and invites tox1...\n"); + + // tox0 makes new group. + Tox_Err_Group_New err_new; + const uint32_t group_number = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PRIVATE, (const uint8_t *)TEST_GROUP_NAME, + TEST_GROUP_NAME_LEN, (const uint8_t *)PEER1_NICK, PEER1_NICK_LEN, &err_new); + + ck_assert(err_new == TOX_ERR_GROUP_NEW_OK); + + // tox0 invites tox1 + Tox_Err_Group_Invite_Friend err_invite; + tox_group_invite_friend(tox0, group_number, 0, &err_invite); + ck_assert(err_invite == TOX_ERR_GROUP_INVITE_FRIEND_OK); + + while (!state0->message_received) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + if (state1->peer_joined && !state1->message_sent) { + tox_group_send_message(tox1, group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)TEST_MESSAGE, + TEST_MESSAGE_LEN, &state1->pseudo_msg_id, &err_send); + ck_assert(err_send == TOX_ERR_GROUP_SEND_MESSAGE_OK); + state1->message_sent = true; + } + } + + ck_assert_msg(state0->pseudo_msg_id == state1->pseudo_msg_id, "id0:%u id1:%u", state0->pseudo_msg_id, state1->pseudo_msg_id); + + // Make sure we're still connected to each friend + Tox_Connection conn_1 = tox_friend_get_connection_status(tox0, 0, nullptr); + Tox_Connection conn_2 = tox_friend_get_connection_status(tox1, 0, nullptr); + + ck_assert(conn_1 != TOX_CONNECTION_NONE && conn_2 != TOX_CONNECTION_NONE); + + // tox0 ignores tox1 + Tox_Err_Group_Set_Ignore ig_err; + tox_group_set_ignore(tox0, group_number, state0->peer_id, true, &ig_err); + ck_assert_msg(ig_err == TOX_ERR_GROUP_SET_IGNORE_OK, "%d", ig_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + // tox1 sends group a message which should not be seen by tox0's message handler + tox_group_send_message(tox1, group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)IGNORE_MESSAGE, + IGNORE_MESSAGE_LEN, nullptr, &err_send); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + // tox0 unignores tox1 + tox_group_set_ignore(tox0, group_number, state0->peer_id, false, &ig_err); + ck_assert_msg(ig_err == TOX_ERR_GROUP_SET_IGNORE_OK, "%d", ig_err); + + fprintf(stderr, "Sending private message...\n"); + + // tox0 sends a private action to tox1 + Tox_Err_Group_Send_Private_Message m_err; + tox_group_send_private_message(tox1, group_number, state1->peer_id, TOX_MESSAGE_TYPE_ACTION, + (const uint8_t *)TEST_PRIVATE_MESSAGE, TEST_PRIVATE_MESSAGE_LEN, &m_err); + ck_assert_msg(m_err == TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK, "%d", m_err); + + fprintf(stderr, "Sending custom packets...\n"); + + // tox0 sends a lossless and lossy custom packet to tox1 + Tox_Err_Group_Send_Custom_Packet c_err; + tox_group_send_custom_packet(tox1, group_number, true, (const uint8_t *)TEST_CUSTOM_PACKET, TEST_CUSTOM_PACKET_LEN, + &c_err); + ck_assert_msg(c_err == TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK, "%d", c_err); + + tox_group_send_custom_packet(tox1, group_number, false, (const uint8_t *)TEST_CUSTOM_PACKET, TEST_CUSTOM_PACKET_LEN, + &c_err); + ck_assert_msg(c_err == TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK, "%d", c_err); + + fprintf(stderr, "Sending custom private packets...\n"); + + // tox0 sends a lossless and lossy custom private packet to tox1 + Tox_Err_Group_Send_Custom_Private_Packet cperr; + tox_group_send_custom_private_packet(tox1, group_number, state1->peer_id, true, + (const uint8_t *)TEST_CUSTOM_PRIVATE_PACKET, + TEST_CUSTOM_PRIVATE_PACKET_LEN, &cperr); + + ck_assert_msg(cperr == TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK, "%d", cperr); + + tox_group_send_custom_private_packet(tox1, group_number, state1->peer_id, false, + (const uint8_t *)TEST_CUSTOM_PRIVATE_PACKET, + TEST_CUSTOM_PRIVATE_PACKET_LEN, &cperr); + + ck_assert_msg(cperr == TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK, "%d", cperr); + + while (!state0->private_message_received || state0->custom_packets_received < 2 + || state0->custom_private_packets_received < 2) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + uint8_t m[TOX_GROUP_MAX_MESSAGE_LENGTH] = {0}; + + fprintf(stderr, "Doing lossless packet test...\n"); + + tox_callback_group_message(tox1, group_message_handler_lossless_test); + state1->last_msg_recv = -1; + + // lossless and packet splitting/reassembly test + for (uint16_t i = 0; i <= MAX_NUM_MESSAGES_LOSSLESS_TEST; ++i) { + if (i % 10 == 0) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + uint16_t message_size = min_u16(4 + (random_u16(rng) % TOX_GROUP_MAX_MESSAGE_LENGTH), TOX_GROUP_MAX_MESSAGE_LENGTH); + + memcpy(m, &i, sizeof(uint16_t)); + + for (size_t j = 4; j < message_size; ++j) { + m[j] = random_u32(rng); + } + + const uint16_t checksum = get_message_checksum(m + 4, message_size - 4); + + memcpy(m + 2, &checksum, sizeof(uint16_t)); + + tox_group_send_message(tox0, group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)m, message_size, nullptr, &err_send); + + ck_assert(err_send == TOX_ERR_GROUP_SEND_MESSAGE_OK); + } + + while (!state1->lossless_check) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + state1->last_msg_recv = -1; + tox_callback_group_message(tox1, group_message_handler_wraparound_test); + + fprintf(stderr, "Doing wraparound test...\n"); + + // packet array wrap-around test + for (uint16_t i = 0; i <= MAX_NUM_MESSAGES_WRAPAROUND_TEST; ++i) { + if (i % 10 == 0) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + memcpy(m, &i, sizeof(uint16_t)); + + tox_group_send_message(tox0, group_number, TOX_MESSAGE_TYPE_NORMAL, (const uint8_t *)m, 2, nullptr, &err_send); + ck_assert(err_send == TOX_ERR_GROUP_SEND_MESSAGE_OK); + } + + while (!state1->wraparound_check) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[i].tox, group_number, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + } + + fprintf(stderr, "All tests passed!\n"); +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_message_test, sizeof(State), &autotest_opts); + return 0; +} + +#undef NUM_GROUP_TOXES +#undef PEER1_NICK +#undef PEER1_NICK_LEN +#undef PEER0_NICK +#undef PEER0_NICK_LEN +#undef TEST_GROUP_NAME +#undef TEST_GROUP_NAME_LEN +#undef TEST_MESSAGE +#undef TEST_MESSAGE_LEN +#undef TEST_PRIVATE_MESSAGE_LEN +#undef TEST_CUSTOM_PACKET +#undef TEST_CUSTOM_PACKET_LEN +#undef TEST_CUSTOM_PRIVATE_PACKET +#undef TEST_CUSTOM_PRIVATE_PACKET_LEN +#undef IGNORE_MESSAGE +#undef IGNORE_MESSAGE_LEN +#undef MAX_NUM_MESSAGES_LOSSLESS_TEST +#undef MAX_NUM_MESSAGES_WRAPAROUND_TEST diff --git a/auto_tests/group_moderation_test.c b/auto_tests/group_moderation_test.c new file mode 100644 index 0000000000..a53609feba --- /dev/null +++ b/auto_tests/group_moderation_test.c @@ -0,0 +1,653 @@ +/* + * Tests group moderation functionality. + * + * Note that making the peer count too high will break things. This test should not be relied on + * for general group/syncing functionality. + */ + +#include +#include +#include + +#include "auto_test_support.h" +#include "check_compat.h" + +#include "../toxcore/tox.h" + +#define NUM_GROUP_TOXES 5 +#define GROUP_NAME "NASA Headquarters" +#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) + +typedef struct Peer { + char name[TOX_MAX_NAME_LENGTH]; + size_t name_length; + uint32_t peer_id; +} Peer; + +typedef struct State { + char self_name[TOX_MAX_NAME_LENGTH]; + size_t self_name_length; + + uint32_t group_number; + + uint32_t num_peers; + Peer peers[NUM_GROUP_TOXES - 1]; + + bool mod_check; + size_t mod_event_count; + char mod_name1[TOX_MAX_NAME_LENGTH]; + char mod_name2[TOX_MAX_NAME_LENGTH]; + + + bool observer_check; + size_t observer_event_count; + char observer_name1[TOX_MAX_NAME_LENGTH]; + char observer_name2[TOX_MAX_NAME_LENGTH]; + + bool user_check; + size_t user_event_count; + + bool kick_check; // mod gets kicked +} State; + +static bool all_peers_connected(AutoTox *autotoxes) +{ + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + + if (state->num_peers != NUM_GROUP_TOXES - 1) { + return false; + } + + if (!tox_group_is_connected(autotoxes[i].tox, state->group_number, nullptr)) { + return false; + } + } + + return true; +} + +/* + * Waits for all peers to receive the mod event. + */ +static void check_mod_event(AutoTox *autotoxes, size_t num_peers, Tox_Group_Mod_Event event) +{ + uint32_t peers_recv_changes = 0; + + do { + peers_recv_changes = 0; + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + for (size_t i = 0; i < num_peers; ++i) { + State *state = (State *)autotoxes[i].state; + bool check = false; + + switch (event) { + case TOX_GROUP_MOD_EVENT_MODERATOR: { + if (state->mod_check) { + check = true; + state->mod_check = false; + } + + break; + } + + case TOX_GROUP_MOD_EVENT_OBSERVER: { + if (state->observer_check) { + check = true; + state->observer_check = false; + } + + break; + } + + case TOX_GROUP_MOD_EVENT_USER: { + if (state->user_check) { + check = true; + state->user_check = false; + } + + break; + } + + case TOX_GROUP_MOD_EVENT_KICK: { + check = state->kick_check; + break; + } + + default: { + ck_assert(0); + } + } + + if (check) { + ++peers_recv_changes; + } + } + } while (peers_recv_changes < num_peers - 1); +} + +static uint32_t get_peer_id_by_nick(const Peer *peers, uint32_t num_peers, const char *name) +{ + ck_assert(name != nullptr); + + for (uint32_t i = 0; i < num_peers; ++i) { + if (memcmp(peers[i].name, name, peers[i].name_length) == 0) { + return peers[i].peer_id; + } + } + + ck_assert_msg(0, "Failed to find peer id"); +} + +static size_t get_state_index_by_nick(const AutoTox *autotoxes, size_t num_peers, const char *name, size_t name_length) +{ + ck_assert(name != nullptr && name_length <= TOX_MAX_NAME_LENGTH); + + for (size_t i = 0; i < num_peers; ++i) { + State *state = (State *)autotoxes[i].state; + + if (memcmp(state->self_name, name, name_length) == 0) { + return i; + } + } + + ck_assert_msg(0, "Failed to find index"); +} + +static void group_join_fail_handler(Tox *tox, uint32_t group_number, Tox_Group_Join_Fail fail_type, void *user_data) +{ + fprintf(stderr, "Failed to join group: %d", fail_type); +} + +static void group_peer_join_handler(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(state->group_number == group_number); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, group_number, peer_id, &q_err); + + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + tox_group_peer_get_name(tox, group_number, peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + + Peer *peer = &state->peers[state->num_peers]; + + peer->peer_id = peer_id; + memcpy(peer->name, peer_name, peer_name_len); + peer->name_length = peer_name_len; + + ++state->num_peers; + + ck_assert(state->num_peers < NUM_GROUP_TOXES); +} + +static void handle_mod(State *state, const char *peer_name, size_t peer_name_len, Tox_Group_Role role) +{ + if (state->mod_event_count == 0) { + ck_assert(memcmp(peer_name, state->mod_name1, peer_name_len) == 0); + } else if (state->mod_event_count == 1) { + ck_assert(memcmp(peer_name, state->mod_name2, peer_name_len) == 0); + } else { + ck_assert(false); + } + + ++state->mod_event_count; + state->mod_check = true; + ck_assert(role == TOX_GROUP_ROLE_MODERATOR); +} + +static void handle_observer(State *state, const char *peer_name, size_t peer_name_len, Tox_Group_Role role) +{ + if (state->observer_event_count == 0) { + ck_assert(memcmp(peer_name, state->observer_name1, peer_name_len) == 0); + } else if (state->observer_event_count == 1) { + ck_assert(memcmp(peer_name, state->observer_name2, peer_name_len) == 0); + } else { + ck_assert(false); + } + + ++state->observer_event_count; + state->observer_check = true; + ck_assert(role == TOX_GROUP_ROLE_OBSERVER); +} + +static void handle_user(State *state, const char *peer_name, size_t peer_name_len, Tox_Group_Role role) +{ + // event 1: observer1 gets promoted back to user + // event 2: observer2 gets promoted to moderator + // event 3: moderator 1 gets kicked + // event 4: moderator 2 gets demoted to moderator + if (state->user_event_count == 0) { + ck_assert(memcmp(peer_name, state->observer_name1, peer_name_len) == 0); + } else if (state->user_event_count == 1) { + ck_assert(memcmp(peer_name, state->observer_name2, peer_name_len) == 0); + } else if (state->user_event_count == 2) { + ck_assert(memcmp(peer_name, state->mod_name1, peer_name_len) == 0); + } else if (state->user_event_count == 3) { + ck_assert(memcmp(peer_name, state->mod_name2, peer_name_len) == 0); + } else { + ck_assert(false); + } + + ++state->user_event_count; + state->user_check = true; + ck_assert(role == TOX_GROUP_ROLE_USER); +} + +static void group_mod_event_handler(Tox *tox, uint32_t group_number, uint32_t source_peer_id, uint32_t target_peer_id, + Tox_Group_Mod_Event event, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(state->group_number == group_number); + + char peer_name[TOX_MAX_NAME_LENGTH + 1]; + + Tox_Err_Group_Peer_Query q_err; + size_t peer_name_len = tox_group_peer_get_name_size(tox, group_number, target_peer_id, &q_err); + + if (q_err == TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND) { // may occurr on sync attempts + return; + } + + ck_assert_msg(q_err == TOX_ERR_GROUP_PEER_QUERY_OK, "error %d", q_err); + ck_assert(peer_name_len <= TOX_MAX_NAME_LENGTH); + + tox_group_peer_get_name(tox, group_number, target_peer_id, (uint8_t *) peer_name, &q_err); + peer_name[peer_name_len] = 0; + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + + Tox_Group_Role role = tox_group_peer_get_role(tox, group_number, target_peer_id, &q_err); + ck_assert(q_err == TOX_ERR_GROUP_PEER_QUERY_OK); + + switch (event) { + case TOX_GROUP_MOD_EVENT_MODERATOR: { + handle_mod(state, peer_name, peer_name_len, role); + break; + } + + case TOX_GROUP_MOD_EVENT_OBSERVER: { + handle_observer(state, peer_name, peer_name_len, role); + break; + } + + case TOX_GROUP_MOD_EVENT_USER: { + handle_user(state, peer_name, peer_name_len, role); + break; + } + + case TOX_GROUP_MOD_EVENT_KICK: { + ck_assert(memcmp(peer_name, state->mod_name1, peer_name_len) == 0); + state->kick_check = true; + break; + } + + default: { + ck_assert_msg(0, "Got invalid moderator event %d", event); + return; + } + } +} + +/* Checks that `peer_id` sees itself with the role `role`. */ +static void check_self_role(AutoTox *autotoxes, uint32_t peer_id, Tox_Group_Role role) +{ + Tox_Err_Group_Self_Query sq_err; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + + uint32_t self_peer_id = tox_group_self_get_peer_id(autotoxes[i].tox, state->group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (self_peer_id == peer_id) { + Tox_Group_Role self_role = tox_group_self_get_role(autotoxes[i].tox, state->group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_role == role); + return; + } + } +} + +/* Makes sure that a peer's role respects the voice state */ +static void voice_state_message_test(AutoTox *autotox, Tox_Group_Voice_State voice_state) +{ + const State *state = (State *)autotox->state; + + Tox_Err_Group_Self_Query sq_err; + Tox_Group_Role self_role = tox_group_self_get_role(autotox->tox, state->group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + Tox_Err_Group_Send_Message msg_err; + bool send_ret = tox_group_send_message(autotox->tox, state->group_number, TOX_MESSAGE_TYPE_NORMAL, + (const uint8_t *)"test", 4, nullptr, &msg_err); + + switch (self_role) { + case TOX_GROUP_ROLE_OBSERVER: { + ck_assert(!send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); + break; + } + + case TOX_GROUP_ROLE_USER: { + if (voice_state != TOX_GROUP_VOICE_STATE_ALL) { + ck_assert_msg(!send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS, + "%d, %d", send_ret, msg_err); + } else { + ck_assert(send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_OK); + } + + break; + } + + case TOX_GROUP_ROLE_MODERATOR: { + if (voice_state != TOX_GROUP_VOICE_STATE_FOUNDER) { + ck_assert(send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_OK); + } else { + ck_assert(!send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); + } + + break; + } + + case TOX_GROUP_ROLE_FOUNDER: { + ck_assert(send_ret && msg_err == TOX_ERR_GROUP_SEND_MESSAGE_OK); + break; + } + } +} + +static bool all_peers_got_voice_state_change(AutoTox *autotoxes, uint32_t num_toxes, + Tox_Group_Voice_State expected_voice_state) +{ + Tox_Err_Group_State_Queries query_err; + + for (uint32_t i = 0; i < num_toxes; ++i) { + const State *state = (State *)autotoxes[i].state; + + Tox_Group_Voice_State voice_state = tox_group_get_voice_state(autotoxes[i].tox, state->group_number, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (voice_state != expected_voice_state) { + return false; + } + } + + return true; +} + +static void check_voice_state(AutoTox *autotoxes, uint32_t num_toxes) +{ + // founder sets voice state to Moderator + const State *state = (State *)autotoxes[0].state; + Tox_Err_Group_Founder_Set_Voice_State voice_set_err; + tox_group_founder_set_voice_state(autotoxes[0].tox, state->group_number, TOX_GROUP_VOICE_STATE_MODERATOR, + &voice_set_err); + ck_assert(voice_set_err == TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); + + for (uint32_t i = 0; i < num_toxes; ++i) { + do { + iterate_all_wait(autotoxes, num_toxes, ITERATION_INTERVAL); + } while (!all_peers_got_voice_state_change(autotoxes, num_toxes, TOX_GROUP_VOICE_STATE_MODERATOR)); + + voice_state_message_test(&autotoxes[i], TOX_GROUP_VOICE_STATE_MODERATOR); + } + + tox_group_founder_set_voice_state(autotoxes[0].tox, state->group_number, TOX_GROUP_VOICE_STATE_FOUNDER, &voice_set_err); + ck_assert(voice_set_err == TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); + + for (uint32_t i = 0; i < num_toxes; ++i) { + do { + iterate_all_wait(autotoxes, num_toxes, ITERATION_INTERVAL); + } while (!all_peers_got_voice_state_change(autotoxes, num_toxes, TOX_GROUP_VOICE_STATE_FOUNDER)); + + voice_state_message_test(&autotoxes[i], TOX_GROUP_VOICE_STATE_FOUNDER); + } + + tox_group_founder_set_voice_state(autotoxes[0].tox, state->group_number, TOX_GROUP_VOICE_STATE_ALL, &voice_set_err); + ck_assert(voice_set_err == TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); + + for (uint32_t i = 0; i < num_toxes; ++i) { + do { + iterate_all_wait(autotoxes, num_toxes, ITERATION_INTERVAL); + } while (!all_peers_got_voice_state_change(autotoxes, num_toxes, TOX_GROUP_VOICE_STATE_ALL)); + + voice_state_message_test(&autotoxes[i], TOX_GROUP_VOICE_STATE_ALL); + } +} + +static void group_moderation_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES >= 4, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + ck_assert_msg(NUM_GROUP_TOXES < 10, "NUM_GROUP_TOXES is too big: %d", NUM_GROUP_TOXES); + + uint16_t name_length = 6; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + state->self_name_length = name_length; + snprintf(state->self_name, sizeof(state->self_name), "peer_%zu", i); + state->self_name[name_length] = 0; + + tox_callback_group_join_fail(autotoxes[i].tox, group_join_fail_handler); + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + tox_callback_group_moderation(autotoxes[i].tox, group_mod_event_handler); + } + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + fprintf(stderr, "Creating new group\n"); + + /* Founder makes new group */ + State *state0 = (State *)autotoxes[0].state; + Tox *tox0 = autotoxes[0].tox; + + Tox_Err_Group_New err_new; + state0->group_number = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, + GROUP_NAME_LEN, (const uint8_t *)state0->self_name, state0->self_name_length, + &err_new); + + ck_assert_msg(err_new == TOX_ERR_GROUP_NEW_OK, "Failed to create group. error: %d\n", err_new); + + /* Founder gets chat ID */ + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + tox_group_get_chat_id(tox0, state0->group_number, chat_id, &id_err); + + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get chat ID. error: %d", id_err); + + fprintf(stderr, "Peers attemping to join DHT group via the chat ID\n"); + + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + State *state = (State *)autotoxes[i].state; + Tox_Err_Group_Join join_err; + state->group_number = tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)state->self_name, + state->self_name_length, + nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "Peer %s (%zu) failed to join group. error %d", + state->self_name, i, join_err); + + c_sleep(100); + } + + // make sure every peer sees every other peer before we continue + do { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } while (!all_peers_connected(autotoxes)); + + /* manually tell the other peers the names of the peers that will be assigned new roles */ + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + memcpy(state->mod_name1, state0->peers[0].name, sizeof(state->mod_name1)); + memcpy(state->mod_name2, state0->peers[2].name, sizeof(state->mod_name2)); + memcpy(state->observer_name1, state0->peers[1].name, sizeof(state->observer_name1)); + memcpy(state->observer_name2, state0->peers[2].name, sizeof(state->observer_name2)); + } + + /* founder checks his own role */ + Tox_Err_Group_Self_Query sq_err; + Tox_Group_Role self_role = tox_group_self_get_role(tox0, state0->group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_role == TOX_GROUP_ROLE_FOUNDER); + + /* all peers should be user role except founder */ + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + State *state = (State *)autotoxes[i].state; + self_role = tox_group_self_get_role(autotoxes[i].tox, state->group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + ck_assert(self_role == TOX_GROUP_ROLE_USER); + } + + /* founder sets first peer to moderator */ + fprintf(stderr, "Founder setting %s to moderator\n", state0->peers[0].name); + + Tox_Err_Group_Mod_Set_Role role_err; + tox_group_mod_set_role(tox0, state0->group_number, state0->peers[0].peer_id, TOX_GROUP_ROLE_MODERATOR, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set moderator. error: %d", role_err); + + // manually flag the role setter because they don't get a callback + state0->mod_check = true; + ++state0->mod_event_count; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_MODERATOR); + + check_self_role(autotoxes, state0->peers[0].peer_id, TOX_GROUP_ROLE_MODERATOR); + + fprintf(stderr, "All peers successfully received mod event\n"); + + /* founder sets second and third peer to observer */ + fprintf(stderr, "Founder setting %s to observer\n", state0->peers[1].name); + + tox_group_mod_set_role(tox0, state0->group_number, state0->peers[1].peer_id, TOX_GROUP_ROLE_OBSERVER, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set observer. error: %d", role_err); + + state0->observer_check = true; + ++state0->observer_event_count; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_OBSERVER); + + fprintf(stderr, "All peers successfully received observer event 1\n"); + + fprintf(stderr, "Founder setting %s to observer\n", state0->peers[2].name); + + tox_group_mod_set_role(tox0, state0->group_number, state0->peers[2].peer_id, TOX_GROUP_ROLE_OBSERVER, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set observer. error: %d", role_err); + + state0->observer_check = true; + ++state0->observer_event_count; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_OBSERVER); + + check_self_role(autotoxes, state0->peers[1].peer_id, TOX_GROUP_ROLE_OBSERVER); + + fprintf(stderr, "All peers successfully received observer event 2\n"); + + /* do voice state test here since we have at least one peer of each role */ + check_voice_state(autotoxes, NUM_GROUP_TOXES); + + fprintf(stderr, "Voice state respected by all peers\n"); + + /* New moderator promotes second peer back to user */ + const uint32_t idx = get_state_index_by_nick(autotoxes, NUM_GROUP_TOXES, state0->peers[0].name, + state0->peers[0].name_length); + State *state1 = (State *)autotoxes[idx].state; + Tox *tox1 = autotoxes[idx].tox; + + const uint32_t obs_peer_id = get_peer_id_by_nick(state1->peers, NUM_GROUP_TOXES - 1, state1->observer_name1); + + fprintf(stderr, "%s is promoting %s back to user\n", state1->self_name, state0->peers[1].name); + + tox_group_mod_set_role(tox1, state1->group_number, obs_peer_id, TOX_GROUP_ROLE_USER, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to promote observer back to user. error: %d", + role_err); + + state1->user_check = true; + ++state1->user_event_count; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_USER); + + fprintf(stderr, "All peers successfully received user event\n"); + + /* founder assigns third peer to moderator (this triggers two events: user and moderator) */ + fprintf(stderr, "Founder setting %s to moderator\n", state0->peers[2].name); + + tox_group_mod_set_role(tox0, state0->group_number, state0->peers[2].peer_id, TOX_GROUP_ROLE_MODERATOR, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set moderator. error: %d", role_err); + + state0->mod_check = true; + ++state0->mod_event_count; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_MODERATOR); + + check_self_role(autotoxes, state0->peers[2].peer_id, TOX_GROUP_ROLE_MODERATOR); + + fprintf(stderr, "All peers successfully received moderator event\n"); + + /* moderator attempts to demote and kick founder */ + uint32_t founder_peer_id = get_peer_id_by_nick(state1->peers, NUM_GROUP_TOXES - 1, state0->self_name); + tox_group_mod_set_role(tox1, state1->group_number, founder_peer_id, TOX_GROUP_ROLE_OBSERVER, &role_err); + ck_assert_msg(role_err != TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Mod set founder to observer"); + + Tox_Err_Group_Mod_Kick_Peer k_err; + tox_group_mod_kick_peer(tox1, state1->group_number, founder_peer_id, &k_err); + ck_assert_msg(k_err != TOX_ERR_GROUP_MOD_KICK_PEER_OK, "Mod kicked founder"); + + /* founder kicks moderator (this triggers two events: user and kick) */ + fprintf(stderr, "Founder is kicking %s\n", state0->peers[0].name); + + tox_group_mod_kick_peer(tox0, state0->group_number, state0->peers[0].peer_id, &k_err); + ck_assert_msg(k_err == TOX_ERR_GROUP_MOD_KICK_PEER_OK, "Failed to kick peer. error: %d", k_err); + + state0->kick_check = true; + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_KICK); + + fprintf(stderr, "All peers successfully received kick event\n"); + + fprintf(stderr, "Founder is demoting moderator to user\n"); + + tox_group_mod_set_role(tox0, state0->group_number, state0->peers[2].peer_id, TOX_GROUP_ROLE_USER, &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to demote peer 3 to User. error: %d", role_err); + + state0->user_check = true; + ++state0->user_event_count; + + check_mod_event(autotoxes, NUM_GROUP_TOXES, TOX_GROUP_MOD_EVENT_USER); + check_self_role(autotoxes, state0->peers[2].peer_id, TOX_GROUP_ROLE_USER); + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + const State *state = (const State *)autotoxes[i].state; + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[i].tox, state->group_number, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + } + + fprintf(stderr, "All tests passed!\n"); +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options options = default_run_auto_options(); + options.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_moderation_test, sizeof(State), &options); + return 0; +} + +#undef NUM_GROUP_TOXES +#undef GROUP_NAME +#undef GROUP_NAME_LEN diff --git a/auto_tests/group_save_test.c b/auto_tests/group_save_test.c new file mode 100644 index 0000000000..0cca6cd9a9 --- /dev/null +++ b/auto_tests/group_save_test.c @@ -0,0 +1,300 @@ +/* + * Tests that we can save a groupchat and load a groupchat with the saved data. + */ + +#include +#include +#include +#include + +#include "auto_test_support.h" + +typedef struct State { + bool peer_joined; +} State; + +#define NUM_GROUP_TOXES 2 +#define GROUP_NAME "The Test Chamber" +#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) +#define TOPIC "They're waiting for you Gordon..." +#define TOPIC_LEN (sizeof(TOPIC) - 1) +#define NEW_PRIV_STATE TOX_GROUP_PRIVACY_STATE_PRIVATE +#define PASSWORD "password123" +#define PASS_LEN (sizeof(PASSWORD) - 1) +#define PEER_LIMIT 69 +#define PEER0_NICK "Mike" +#define PEER0_NICK_LEN (sizeof(PEER0_NICK) -1) +#define NEW_USER_STATUS TOX_USER_STATUS_BUSY + +static void group_invite_handler(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *user_data) +{ + Tox_Err_Group_Invite_Accept err_accept; + tox_group_invite_accept(tox, friend_number, invite_data, length, (const uint8_t *)"test2", 5, + nullptr, 0, &err_accept); + ck_assert(err_accept == TOX_ERR_GROUP_INVITE_ACCEPT_OK); + +} + +static void group_peer_join_handler(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + state->peer_joined = true; +} + +/* Checks that group has the same state according to the above defines + * + * Returns 0 if state is correct. + * Returns a value < 0 if state is incorrect. + */ +static int has_correct_group_state(const Tox *tox, uint32_t group_number, const uint8_t *expected_chat_id) +{ + Tox_Err_Group_State_Queries query_err; + + Tox_Group_Privacy_State priv_state = tox_group_get_privacy_state(tox, group_number, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (priv_state != NEW_PRIV_STATE) { + return -1; + } + + size_t pass_len = tox_group_get_password_size(tox, group_number, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + uint8_t password[TOX_GROUP_MAX_PASSWORD_SIZE]; + tox_group_get_password(tox, group_number, password, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (pass_len != PASS_LEN || memcmp(password, PASSWORD, pass_len) != 0) { + return -2; + } + + size_t gname_len = tox_group_get_name_size(tox, group_number, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + uint8_t group_name[TOX_GROUP_MAX_GROUP_NAME_LENGTH]; + tox_group_get_name(tox, group_number, group_name, &query_err); + + if (gname_len != GROUP_NAME_LEN || memcmp(group_name, GROUP_NAME, gname_len) != 0) { + return -3; + } + + if (tox_group_get_peer_limit(tox, group_number, nullptr) != PEER_LIMIT) { + return -4; + } + + Tox_Group_Topic_Lock topic_lock = tox_group_get_topic_lock(tox, group_number, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (topic_lock != TOX_GROUP_TOPIC_LOCK_DISABLED) { + return -5; + } + + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + tox_group_get_chat_id(tox, group_number, chat_id, &id_err); + + ck_assert(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (memcmp(chat_id, expected_chat_id, TOX_GROUP_CHAT_ID_SIZE) != 0) { + return -6; + } + + return 0; +} + +static int has_correct_self_state(const Tox *tox, uint32_t group_number, const uint8_t *expected_self_pk) +{ + Tox_Err_Group_Self_Query sq_err; + size_t self_length = tox_group_self_get_name_size(tox, group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + uint8_t self_name[TOX_MAX_NAME_LENGTH]; + tox_group_self_get_name(tox, group_number, self_name, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (self_length != PEER0_NICK_LEN || memcmp(self_name, PEER0_NICK, self_length) != 0) { + return -1; + } + + TOX_USER_STATUS self_status = tox_group_self_get_status(tox, group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (self_status != NEW_USER_STATUS) { + return -2; + } + + Tox_Group_Role self_role = tox_group_self_get_role(tox, group_number, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (self_role != TOX_GROUP_ROLE_FOUNDER) { + return -3; + } + + uint8_t self_pk[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + + tox_group_self_get_public_key(tox, group_number, self_pk, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + if (memcmp(self_pk, expected_self_pk, TOX_GROUP_PEER_PUBLIC_KEY_SIZE) != 0) { + return -4; + } + + return 0; +} + +static void group_save_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES > 1, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_invite(autotoxes[i].tox, group_invite_handler); + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + } + + Tox *tox0 = autotoxes[0].tox; + + const State *state0 = (State *)autotoxes[0].state; + const State *state1 = (State *)autotoxes[1].state; + + Tox_Err_Group_New err_new; + const uint32_t group_number = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PRIVATE, (const uint8_t *)GROUP_NAME, + GROUP_NAME_LEN, (const uint8_t *)"test", 4, &err_new); + + ck_assert(err_new == TOX_ERR_GROUP_NEW_OK); + + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + + Tox_Err_Group_State_Queries id_err; + tox_group_get_chat_id(tox0, group_number, chat_id, &id_err); + ck_assert(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + uint8_t founder_pk[TOX_GROUP_PEER_PUBLIC_KEY_SIZE]; + + Tox_Err_Group_Self_Query sq_err; + tox_group_self_get_public_key(tox0, group_number, founder_pk, &sq_err); + ck_assert(sq_err == TOX_ERR_GROUP_SELF_QUERY_OK); + + + Tox_Err_Group_Invite_Friend err_invite; + tox_group_invite_friend(tox0, group_number, 0, &err_invite); + + ck_assert(err_invite == TOX_ERR_GROUP_INVITE_FRIEND_OK); + + while (!state0->peer_joined && !state1->peer_joined) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + printf("tox0 invites tox1 to group\n"); + + // change group state + Tox_Err_Group_Topic_Set top_err; + tox_group_set_topic(tox0, group_number, (const uint8_t *)TOPIC, TOPIC_LEN, &top_err); + ck_assert(top_err == TOX_ERR_GROUP_TOPIC_SET_OK); + + Tox_Err_Group_Founder_Set_Topic_Lock lock_set_err; + tox_group_founder_set_topic_lock(tox0, group_number, TOX_GROUP_TOPIC_LOCK_DISABLED, &lock_set_err); + ck_assert(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK); + + Tox_Err_Group_Founder_Set_Privacy_State priv_err; + tox_group_founder_set_privacy_state(tox0, group_number, NEW_PRIV_STATE, &priv_err); + ck_assert(priv_err == TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK); + + Tox_Err_Group_Founder_Set_Password pass_set_err; + tox_group_founder_set_password(tox0, group_number, (const uint8_t *)PASSWORD, PASS_LEN, &pass_set_err); + ck_assert(pass_set_err == TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK); + + Tox_Err_Group_Founder_Set_Peer_Limit limit_set_err; + tox_group_founder_set_peer_limit(tox0, group_number, PEER_LIMIT, &limit_set_err); + ck_assert(limit_set_err == TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK); + + // change self state + Tox_Err_Group_Self_Name_Set n_err; + tox_group_self_set_name(tox0, group_number, (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, &n_err); + ck_assert(n_err == TOX_ERR_GROUP_SELF_NAME_SET_OK); + + Tox_Err_Group_Self_Status_Set s_err; + tox_group_self_set_status(tox0, group_number, NEW_USER_STATUS, &s_err); + ck_assert(s_err == TOX_ERR_GROUP_SELF_STATUS_SET_OK); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + printf("tox0 changes group state\n"); + + const size_t save_length = tox_get_savedata_size(tox0); + + uint8_t *save = (uint8_t *)malloc(save_length); + + ck_assert(save != nullptr); + + tox_get_savedata(tox0, save); + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + tox_group_leave(autotoxes[i].tox, group_number, nullptr, 0, nullptr); + } + + struct Tox_Options *const options = tox_options_new(nullptr); + + ck_assert(options != nullptr); + + tox_options_set_savedata_type(options, TOX_SAVEDATA_TYPE_TOX_SAVE); + + tox_options_set_savedata_data(options, save, save_length); + + Tox *new_tox = tox_new_log(options, nullptr, nullptr); + + ck_assert(new_tox != nullptr); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + printf("tox0 saves group and reloads client\n"); + + const int group_ret = has_correct_group_state(new_tox, group_number, chat_id); + + ck_assert_msg(group_ret == 0, "incorrect group state: %d", group_ret); + + const int self_ret = has_correct_self_state(new_tox, group_number, founder_pk); + + ck_assert_msg(self_ret == 0, "incorrect self state: %d", self_ret); + + tox_group_leave(new_tox, group_number, nullptr, 0, nullptr); + + free(save); + + tox_options_free(options); + + tox_kill(new_tox); + + printf("All tests passed!\n"); + +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_save_test, sizeof(State), &autotest_opts); + + return 0; +} + +#undef NUM_GROUP_TOXES +#undef GROUP_NAME +#undef GROUP_NAME_LEN +#undef TOPIC +#undef TOPIC_LEN +#undef NEW_PRIV_STATE +#undef PASSWORD +#undef PASS_LEN +#undef PEER_LIMIT +#undef PEER0_NICK +#undef PEER0_NICK_LEN +#undef NEW_USER_STATUS diff --git a/auto_tests/group_state_test.c b/auto_tests/group_state_test.c new file mode 100644 index 0000000000..adbe1725fc --- /dev/null +++ b/auto_tests/group_state_test.c @@ -0,0 +1,345 @@ +/* + * Tests that we can successfully change the group state and that all peers in the group + * receive the correct state changes. + */ + +#include +#include +#include +#include +#include + +#include "auto_test_support.h" +#include "check_compat.h" + +#define NUM_GROUP_TOXES 5 + +#define PEER_LIMIT_1 NUM_GROUP_TOXES +#define PEER_LIMIT_2 50 + +#define PASSWORD "dadada" +#define PASS_LEN (sizeof(PASSWORD) - 1) + +#define GROUP_NAME "The Crystal Palace" +#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) + +#define PEER0_NICK "David" +#define PEER0_NICK_LEN (sizeof(PEER0_NICK) - 1) + +typedef struct State { + size_t num_peers; +} State; + +static bool all_group_peers_connected(const AutoTox *autotoxes, uint32_t tox_count, uint32_t groupnumber, + size_t name_length, uint32_t peer_limit) +{ + for (uint32_t i = 0; i < tox_count; ++i) { + // make sure we got an invite response + if (tox_group_get_name_size(autotoxes[i].tox, groupnumber, nullptr) != name_length) { + return false; + } + + // make sure we got a sync response + if (tox_group_get_peer_limit(autotoxes[i].tox, groupnumber, nullptr) != peer_limit) { + return false; + } + + // make sure we're actually connected + if (!tox_group_is_connected(autotoxes[i].tox, groupnumber, nullptr)) { + return false; + } + + const State *state = (const State *)autotoxes[i].state; + + // make sure all peers are connected to one another + if (state->num_peers < NUM_GROUP_TOXES - 1) { + return false; + } + } + + return true; +} + +static void group_topic_lock_handler(Tox *tox, uint32_t groupnumber, Tox_Group_Topic_Lock topic_lock, + void *user_data) +{ + Tox_Err_Group_State_Queries err; + Tox_Group_Topic_Lock current_topic_lock = tox_group_get_topic_lock(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(current_topic_lock == topic_lock, "topic locks don't match in callback: %d %d", + topic_lock, current_topic_lock); +} + +static void group_voice_state_handler(Tox *tox, uint32_t groupnumber, Tox_Group_Voice_State voice_state, + void *user_data) +{ + Tox_Err_Group_State_Queries err; + Tox_Group_Voice_State current_voice_state = tox_group_get_voice_state(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(current_voice_state == voice_state, "voice states don't match in callback: %d %d", + voice_state, current_voice_state); +} + +static void group_privacy_state_handler(Tox *tox, uint32_t groupnumber, Tox_Group_Privacy_State privacy_state, + void *user_data) +{ + Tox_Err_Group_State_Queries err; + Tox_Group_Privacy_State current_pstate = tox_group_get_privacy_state(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(current_pstate == privacy_state, "privacy states don't match in callback"); +} + +static void group_peer_limit_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_limit, void *user_data) +{ + Tox_Err_Group_State_Queries err; + uint32_t current_plimit = tox_group_get_peer_limit(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(peer_limit == current_plimit, + "Peer limits don't match in callback: %u, %u\n", peer_limit, current_plimit); +} + +static void group_password_handler(Tox *tox, uint32_t groupnumber, const uint8_t *password, size_t length, + void *user_data) +{ + Tox_Err_Group_State_Queries err; + size_t curr_pwlength = tox_group_get_password_size(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert(length == curr_pwlength); + + uint8_t current_password[TOX_GROUP_MAX_PASSWORD_SIZE]; + tox_group_get_password(tox, groupnumber, current_password, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(memcmp(current_password, password, length) == 0, + "Passwords don't match: %s, %s", password, current_password); +} + +static void group_peer_join_handler(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ++state->num_peers; + ck_assert(state->num_peers < NUM_GROUP_TOXES); +} + +/* Returns 0 if group state is equal to the state passed to this function. + * Returns negative integer if state is invalid. + */ +static int check_group_state(const Tox *tox, uint32_t groupnumber, uint32_t peer_limit, + Tox_Group_Privacy_State priv_state, Tox_Group_Voice_State voice_state, + const uint8_t *password, size_t pass_len, Tox_Group_Topic_Lock topic_lock) +{ + Tox_Err_Group_State_Queries query_err; + + Tox_Group_Privacy_State my_priv_state = tox_group_get_privacy_state(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get privacy state: %d", query_err); + + if (my_priv_state != priv_state) { + return -1; + } + + uint32_t my_peer_limit = tox_group_get_peer_limit(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get peer limit: %d", query_err); + + if (my_peer_limit != peer_limit) { + return -2; + } + + size_t my_pass_len = tox_group_get_password_size(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get password size: %d", query_err); + + if (my_pass_len != pass_len) { + return -5; + } + + if (password != nullptr && my_pass_len > 0) { + ck_assert(my_pass_len <= TOX_GROUP_MAX_PASSWORD_SIZE); + + uint8_t my_pass[TOX_GROUP_MAX_PASSWORD_SIZE]; + tox_group_get_password(tox, groupnumber, my_pass, &query_err); + my_pass[my_pass_len] = 0; + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get password: %d", query_err); + + if (memcmp(my_pass, password, my_pass_len) != 0) { + return -6; + } + } + + /* Group name should never change */ + size_t my_gname_len = tox_group_get_name_size(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get group name size: %d", query_err); + + if (my_gname_len != GROUP_NAME_LEN) { + return -7; + } + + ck_assert(my_gname_len <= TOX_GROUP_MAX_GROUP_NAME_LENGTH); + + uint8_t my_gname[TOX_GROUP_MAX_GROUP_NAME_LENGTH]; + tox_group_get_name(tox, groupnumber, my_gname, &query_err); + my_gname[my_gname_len] = 0; + + if (memcmp(my_gname, (const uint8_t *)GROUP_NAME, my_gname_len) != 0) { + return -8; + } + + Tox_Group_Topic_Lock current_topic_lock = tox_group_get_topic_lock(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get topic lock: %d", query_err); + + if (current_topic_lock != topic_lock) { + return -9; + } + + Tox_Group_Voice_State current_voice_state = tox_group_get_voice_state(tox, groupnumber, &query_err); + ck_assert_msg(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "Failed to get voice state: %d", query_err); + + if (current_voice_state != voice_state) { + return -10; + } + + return 0; +} + +static void set_group_state(Tox *tox, uint32_t groupnumber, uint32_t peer_limit, Tox_Group_Privacy_State priv_state, + Tox_Group_Voice_State voice_state, const uint8_t *password, size_t pass_len, + Tox_Group_Topic_Lock topic_lock) +{ + + Tox_Err_Group_Founder_Set_Peer_Limit limit_set_err; + tox_group_founder_set_peer_limit(tox, groupnumber, peer_limit, &limit_set_err); + ck_assert_msg(limit_set_err == TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, "failed to set peer limit: %d", limit_set_err); + + Tox_Err_Group_Founder_Set_Privacy_State priv_err; + tox_group_founder_set_privacy_state(tox, groupnumber, priv_state, &priv_err); + ck_assert_msg(priv_err == TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, "failed to set privacy state: %d", priv_err); + + Tox_Err_Group_Founder_Set_Password pass_set_err; + tox_group_founder_set_password(tox, groupnumber, password, pass_len, &pass_set_err); + ck_assert_msg(pass_set_err == TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, "failed to set password: %d", pass_set_err); + + Tox_Err_Group_Founder_Set_Topic_Lock lock_set_err; + tox_group_founder_set_topic_lock(tox, groupnumber, topic_lock, &lock_set_err); + ck_assert_msg(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, "failed to set topic lock: %d", + lock_set_err); + + Tox_Err_Group_Founder_Set_Voice_State voice_set_err; + tox_group_founder_set_voice_state(tox, groupnumber, voice_state, &voice_set_err); + ck_assert_msg(voice_set_err == TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK, "failed to set voice state: %d", + voice_set_err); +} + +static void group_state_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES >= 3, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_privacy_state(autotoxes[i].tox, group_privacy_state_handler); + tox_callback_group_peer_limit(autotoxes[i].tox, group_peer_limit_handler); + tox_callback_group_password(autotoxes[i].tox, group_password_handler); + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + tox_callback_group_voice_state(autotoxes[i].tox, group_voice_state_handler); + tox_callback_group_topic_lock(autotoxes[i].tox, group_topic_lock_handler); + } + + Tox *tox0 = autotoxes[0].tox; + + /* Tox 0 creates a group and is the founder of a newly created group */ + Tox_Err_Group_New new_err; + uint32_t groupnum = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, GROUP_NAME_LEN, + (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, &new_err); + + ck_assert_msg(new_err == TOX_ERR_GROUP_NEW_OK, "tox_group_new failed: %d", new_err); + + /* Founder sets default group state before anyone else joins */ + set_group_state(tox0, groupnum, PEER_LIMIT_1, TOX_GROUP_PRIVACY_STATE_PUBLIC, TOX_GROUP_VOICE_STATE_ALL, + (const uint8_t *)PASSWORD, PASS_LEN, TOX_GROUP_TOPIC_LOCK_ENABLED); + + /* Founder gets the Chat ID and implicitly shares it publicly */ + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + tox_group_get_chat_id(tox0, groupnum, chat_id, &id_err); + + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "tox_group_get_chat_id failed %d", id_err); + + /* All other peers join the group using the Chat ID and password */ + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + Tox_Err_Group_Join join_err; + tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"Test", 4, (const uint8_t *)PASSWORD, PASS_LEN, + &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "tox_group_join failed: %d", join_err); + } + + fprintf(stderr, "Peers attempting to join group\n"); + + /* Keep checking if all instances have connected to the group until test times out */ + while (!all_group_peers_connected(autotoxes, NUM_GROUP_TOXES, groupnum, GROUP_NAME_LEN, PEER_LIMIT_1)) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + /* Change group state and check that all peers received the changes */ + set_group_state(tox0, groupnum, PEER_LIMIT_2, TOX_GROUP_PRIVACY_STATE_PRIVATE, TOX_GROUP_VOICE_STATE_MODERATOR, + nullptr, 0, TOX_GROUP_TOPIC_LOCK_DISABLED); + + fprintf(stderr, "Changing state\n"); + + while (1) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + uint32_t count = 0; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + if (check_group_state(autotoxes[i].tox, groupnum, PEER_LIMIT_2, TOX_GROUP_PRIVACY_STATE_PRIVATE, + TOX_GROUP_VOICE_STATE_MODERATOR, nullptr, 0, TOX_GROUP_TOPIC_LOCK_DISABLED) == 0) { + ++count; + } + } + + if (count == NUM_GROUP_TOXES) { + fprintf(stderr, "%u peers successfully received state changes\n", count); + break; + } + } + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[i].tox, groupnum, nullptr, 0, &err_exit); + ck_assert_msg(err_exit == TOX_ERR_GROUP_LEAVE_OK, "%d", err_exit); + } + + fprintf(stderr, "All tests passed!\n"); + +#endif /* VANILLA_NACL */ +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_state_test, sizeof(State), &autotest_opts); + + return 0; +} + +#undef PEER0_NICK +#undef PEER0_NICK_LEN +#undef GROUP_NAME_LEN +#undef GROUP_NAME +#undef PASS_LEN +#undef PASSWORD +#undef PEER_LIMIT_2 +#undef PEER_LIMIT_1 +#undef NUM_GROUP_TOXES diff --git a/auto_tests/group_sync_test.c b/auto_tests/group_sync_test.c new file mode 100644 index 0000000000..4d2bc1866d --- /dev/null +++ b/auto_tests/group_sync_test.c @@ -0,0 +1,464 @@ +/* + * Tests syncing capabilities of groups: we attempt to have multiple peers change the + * group state in a number of ways and make sure that all peers end up with the same + * resulting state after a short period. + */ + +#include +#include +#include + +#include "auto_test_support.h" + +#include "../toxcore/tox.h" +#include "../toxcore/util.h" + +// these should be kept relatively low so integration tests don't always flake out +// but they can be increased for local stress testing +#define NUM_GROUP_TOXES 5 +#define ROLE_SPAM_ITERATIONS 1 +#define TOPIC_SPAM_ITERATIONS 1 + +typedef struct Peers { + uint32_t num_peers; + int64_t *peer_ids; +} Peers; + +typedef struct State { + uint8_t callback_topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + size_t topic_length; + Peers *peers; +} State; + +static int add_peer(Peers *peers, uint32_t peer_id) +{ + const uint32_t new_idx = peers->num_peers; + + int64_t *tmp_list = (int64_t *)realloc(peers->peer_ids, sizeof(int64_t) * (peers->num_peers + 1)); + + if (tmp_list == nullptr) { + return -1; + } + + ++peers->num_peers; + + tmp_list[new_idx] = (int64_t)peer_id; + + peers->peer_ids = tmp_list; + + return 0; +} + +static int del_peer(Peers *peers, uint32_t peer_id) +{ + bool found_peer = false; + int64_t i; + + for (i = 0; i < peers->num_peers; ++i) { + if (peers->peer_ids[i] == peer_id) { + found_peer = true; + break; + } + } + + if (!found_peer) { + return -1; + } + + --peers->num_peers; + + if (peers->num_peers == 0) { + free(peers->peer_ids); + peers->peer_ids = nullptr; + return 0; + } + + if (peers->num_peers != i) { + peers->peer_ids[i] = peers->peer_ids[peers->num_peers]; + } + + peers->peer_ids[peers->num_peers] = -1; + + int64_t *tmp_list = (int64_t *)realloc(peers->peer_ids, sizeof(int64_t) * (peers->num_peers)); + + if (tmp_list == nullptr) { + return -1; + } + + peers->peer_ids = tmp_list; + + return 0; +} + +static void peers_cleanup(Peers *peers) +{ + free(peers->peer_ids); + free(peers); +} + +static void group_peer_join_handler(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(add_peer(state->peers, peer_id) == 0); + +} + +static void group_peer_exit_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *name, size_t name_length, const uint8_t *part_message, + size_t length, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(del_peer(state->peers, peer_id) == 0); + +} + +static void group_topic_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, const uint8_t *topic, + size_t length, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(length <= TOX_GROUP_MAX_TOPIC_LENGTH); + + memcpy(state->callback_topic, (const char *)topic, length); + state->topic_length = length; +} + +static bool all_peers_connected(const AutoTox *autotoxes, uint32_t groupnumber) +{ + for (uint32_t i = 0; i < NUM_GROUP_TOXES; ++i) { + // make sure we got an invite response + if (tox_group_get_name_size(autotoxes[i].tox, groupnumber, nullptr) != 4) { + return false; + } + + // make sure we're actually connected + if (!tox_group_is_connected(autotoxes[i].tox, groupnumber, nullptr)) { + return false; + } + + const State *state = (const State *)autotoxes[i].state; + + // make sure all peers are connected to one another + if (state->peers->num_peers == NUM_GROUP_TOXES - 1) { + return false; + } + } + + return true; +} + +static unsigned int get_peer_roles_checksum(const Tox *tox, const State *state, uint32_t groupnumber) +{ + Tox_Group_Role role = tox_group_self_get_role(tox, groupnumber, nullptr); + unsigned int checksum = (unsigned int)role; + + for (size_t i = 0; i < state->peers->num_peers; ++i) { + role = tox_group_peer_get_role(tox, groupnumber, (uint32_t)state->peers->peer_ids[i], nullptr); + checksum += (unsigned int)role; + } + + return checksum; +} + +static bool all_peers_see_same_roles(const AutoTox *autotoxes, uint32_t num_peers, uint32_t groupnumber) +{ + const State *state0 = (const State *)autotoxes[0].state; + unsigned int expected_checksum = get_peer_roles_checksum(autotoxes[0].tox, state0, groupnumber); + + for (size_t i = 0; i < num_peers; ++i) { + const State *state = (const State *)autotoxes[i].state; + unsigned int checksum = get_peer_roles_checksum(autotoxes[i].tox, state, groupnumber); + + if (checksum != expected_checksum) { + return false; + } + } + + return true; +} + +static void role_spam(const Random *rng, AutoTox *autotoxes, uint32_t num_peers, uint32_t num_demoted, + uint32_t groupnumber) +{ + const State *state0 = (const State *)autotoxes[0].state; + Tox *tox0 = autotoxes[0].tox; + + for (size_t iters = 0; iters < ROLE_SPAM_ITERATIONS; ++iters) { + // founder randomly promotes or demotes one of the non-mods + uint32_t idx = min_u32(random_u32(rng) % num_demoted, state0->peers->num_peers); + Tox_Group_Role f_role = random_u32(rng) % 2 == 0 ? TOX_GROUP_ROLE_MODERATOR : TOX_GROUP_ROLE_USER; + int64_t peer_id = state0->peers->peer_ids[idx]; + + if (peer_id >= 0) { + tox_group_mod_set_role(tox0, groupnumber, (uint32_t)peer_id, f_role, nullptr); + } + + // mods randomly promote or demote one of the non-mods + for (uint32_t i = 1; i < num_peers; ++i) { + const State *state_i = (const State *)autotoxes[i].state; + + for (uint32_t j = num_demoted; j < num_peers; ++j) { + if (i >= state_i->peers->num_peers) { + continue; + } + + const State *state_j = (const State *)autotoxes[j].state; + Tox_Group_Role role = random_u32(rng) % 2 == 0 ? TOX_GROUP_ROLE_USER : TOX_GROUP_ROLE_OBSERVER; + peer_id = state_j->peers->peer_ids[i]; + + if (peer_id >= 0) { + tox_group_mod_set_role(autotoxes[j].tox, groupnumber, (uint32_t)peer_id, role, nullptr); + } + } + } + + iterate_all_wait(autotoxes, num_peers, ITERATION_INTERVAL); + } + + do { + iterate_all_wait(autotoxes, num_peers, ITERATION_INTERVAL); + } while (!all_peers_see_same_roles(autotoxes, num_peers, groupnumber)); +} + +/* All peers attempt to set a unique topic. + * + * Return true if all peers successfully changed the topic. + */ +static bool set_topic_all_peers(const Random *rng, AutoTox *autotoxes, size_t num_peers, uint32_t groupnumber) +{ + for (size_t i = 0; i < num_peers; ++i) { + char new_topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + snprintf(new_topic, sizeof(new_topic), "peer %zu's topic %u", i, random_u32(rng)); + const size_t length = strlen(new_topic); + + Tox_Err_Group_Topic_Set err; + tox_group_set_topic(autotoxes[i].tox, groupnumber, (const uint8_t *)new_topic, length, &err); + + if (err != TOX_ERR_GROUP_TOPIC_SET_OK) { + return false; + } + } + + return true; +} + +/* Returns true if all peers have the same topic, and the topic from the get_topic API function + * matches the last topic they received in the topic callback. + */ +static bool all_peers_have_same_topic(const AutoTox *autotoxes, uint32_t num_peers, uint32_t groupnumber) +{ + uint8_t expected_topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + + Tox_Err_Group_State_Queries query_err; + size_t expected_topic_length = tox_group_get_topic_size(autotoxes[0].tox, groupnumber, &query_err); + + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + tox_group_get_topic(autotoxes[0].tox, groupnumber, expected_topic, &query_err); + + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + const State *state0 = (const State *)autotoxes[0].state; + + if (expected_topic_length != state0->topic_length) { + return false; + } + + if (memcmp(state0->callback_topic, expected_topic, expected_topic_length) != 0) { + return false; + } + + for (size_t i = 1; i < num_peers; ++i) { + size_t topic_length = tox_group_get_topic_size(autotoxes[i].tox, groupnumber, &query_err); + + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (topic_length != expected_topic_length) { + return false; + } + + uint8_t topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + tox_group_get_topic(autotoxes[i].tox, groupnumber, topic, &query_err); + + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (memcmp(expected_topic, (const char *)topic, topic_length) != 0) { + return false; + } + + const State *state = (const State *)autotoxes[i].state; + + if (topic_length != state->topic_length) { + return false; + } + + if (memcmp(state->callback_topic, (const char *)topic, topic_length) != 0) { + return false; + } + } + + return true; +} + +static void topic_spam(const Random *rng, AutoTox *autotoxes, uint32_t num_peers, uint32_t groupnumber) +{ + for (size_t i = 0; i < TOPIC_SPAM_ITERATIONS; ++i) { + do { + iterate_all_wait(autotoxes, num_peers, ITERATION_INTERVAL); + } while (!set_topic_all_peers(rng, autotoxes, num_peers, groupnumber)); + } + + fprintf(stderr, "all peers set the topic at the same time\n"); + + do { + iterate_all_wait(autotoxes, num_peers, ITERATION_INTERVAL); + } while (!all_peers_have_same_topic(autotoxes, num_peers, groupnumber)); + + fprintf(stderr, "all peers see the same topic\n"); +} + +static void group_sync_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert(NUM_GROUP_TOXES >= 5); + const Random *rng = system_random(); + ck_assert(rng != nullptr); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + tox_callback_group_topic(autotoxes[i].tox, group_topic_handler); + tox_callback_group_peer_exit(autotoxes[i].tox, group_peer_exit_handler); + + State *state = (State *)autotoxes[i].state; + state->peers = (Peers *)calloc(1, sizeof(Peers)); + + ck_assert(state->peers != nullptr); + } + + Tox *tox0 = autotoxes[0].tox; + State *state0 = (State *)autotoxes[0].state; + + Tox_Err_Group_New err_new; + uint32_t groupnumber = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *) "test", 4, + (const uint8_t *)"test", 4, &err_new); + + ck_assert(err_new == TOX_ERR_GROUP_NEW_OK); + + fprintf(stderr, "tox0 creats new group and invites all his friends"); + + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + + tox_group_get_chat_id(tox0, groupnumber, chat_id, &id_err); + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "%d", id_err); + + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + Tox_Err_Group_Join join_err; + tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "%d", join_err); + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } + + do { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } while (!all_peers_connected(autotoxes, groupnumber)); + + fprintf(stderr, "%d peers joined the group\n", NUM_GROUP_TOXES); + + Tox_Err_Group_Founder_Set_Topic_Lock lock_set_err; + tox_group_founder_set_topic_lock(tox0, groupnumber, TOX_GROUP_TOPIC_LOCK_DISABLED, &lock_set_err); + ck_assert_msg(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, "failed to disable topic lock: %d", + lock_set_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + fprintf(stderr, "founder disabled topic lock; all peers try to set the topic\n"); + + topic_spam(rng, autotoxes, NUM_GROUP_TOXES, groupnumber); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + tox_group_founder_set_topic_lock(tox0, groupnumber, TOX_GROUP_TOPIC_LOCK_ENABLED, &lock_set_err); + ck_assert_msg(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, "failed to enable topic lock: %d", + lock_set_err); + + do { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } while (!all_peers_have_same_topic(autotoxes, NUM_GROUP_TOXES, groupnumber) + && !all_peers_see_same_roles(autotoxes, NUM_GROUP_TOXES, groupnumber) + && state0->peers->num_peers != NUM_GROUP_TOXES - 1); + + Tox_Err_Group_Mod_Set_Role role_err; + + for (size_t i = 0; i < state0->peers->num_peers; ++i) { + tox_group_mod_set_role(tox0, groupnumber, (uint32_t)state0->peers->peer_ids[i], TOX_GROUP_ROLE_MODERATOR, + &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set moderator. error: %d", role_err); + } + + fprintf(stderr, "founder enabled topic lock and set all peers to moderator role\n"); + + do { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } while (!all_peers_see_same_roles(autotoxes, NUM_GROUP_TOXES, groupnumber)); + + topic_spam(rng, autotoxes, NUM_GROUP_TOXES, groupnumber); + + const unsigned int num_demoted = state0->peers->num_peers / 2; + + fprintf(stderr, "founder demoting %u moderators to user\n", num_demoted); + + for (size_t i = 0; i < num_demoted; ++i) { + tox_group_mod_set_role(tox0, groupnumber, (uint32_t)state0->peers->peer_ids[i], TOX_GROUP_ROLE_USER, + &role_err); + ck_assert_msg(role_err == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set user. error: %d", role_err); + } + + do { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + } while (!all_peers_see_same_roles(autotoxes, NUM_GROUP_TOXES, groupnumber)); + + fprintf(stderr, "Remaining moderators spam change non-moderator roles\n"); + + role_spam(rng, autotoxes, NUM_GROUP_TOXES, num_demoted, groupnumber); + + fprintf(stderr, "All peers see the same roles\n"); + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + tox_group_leave(autotoxes[i].tox, groupnumber, nullptr, 0, nullptr); + + State *state = (State *)autotoxes[i].state; + peers_cleanup(state->peers); + } + + fprintf(stderr, "All tests passed!\n"); + +#endif // VANILLA_NACL +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_sync_test, sizeof(State), &autotest_opts); + + return 0; +} + +#undef NUM_GROUP_TOXES +#undef ROLE_SPAM_ITERATIONS +#undef TOPIC_SPAM_ITERATIONS diff --git a/auto_tests/group_tcp_test.c b/auto_tests/group_tcp_test.c new file mode 100644 index 0000000000..c1f66de928 --- /dev/null +++ b/auto_tests/group_tcp_test.c @@ -0,0 +1,255 @@ +/* + * Does a basic functionality test for TCP connections. + */ + +#include +#include +#include + +#include "auto_test_support.h" + +#ifdef USE_TEST_NETWORK + +#define NUM_GROUP_TOXES 2 +#define CODEWORD "RONALD MCDONALD" +#define CODEWORD_LEN (sizeof(CODEWORD) - 1) + +typedef struct State { + size_t num_peers; + bool got_code; + bool got_second_code; + uint32_t peer_id[NUM_GROUP_TOXES - 1]; +} State; + +static void group_invite_handler(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *user_data) +{ + printf("Accepting friend invite\n"); + + Tox_Err_Group_Invite_Accept err_accept; + tox_group_invite_accept(tox, friend_number, invite_data, length, (const uint8_t *)"test", 4, + nullptr, 0, &err_accept); + ck_assert(err_accept == TOX_ERR_GROUP_INVITE_ACCEPT_OK); +} + +static void group_peer_join_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + fprintf(stderr, "joined: %zu, %u\n", state->num_peers, peer_id); + ck_assert_msg(state->num_peers < NUM_GROUP_TOXES - 1, "%zu", state->num_peers); + + state->peer_id[state->num_peers++] = peer_id; +} + +static void group_private_message_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(length == CODEWORD_LEN); + ck_assert(memcmp(CODEWORD, message, length) == 0); + + printf("Codeword: %s\n", CODEWORD); + + state->got_code = true; +} + +static void group_message_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, TOX_MESSAGE_TYPE type, + const uint8_t *message, size_t length, uint32_t message_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + ck_assert(length == CODEWORD_LEN); + ck_assert(memcmp(CODEWORD, message, length) == 0); + + printf("Codeword: %s\n", CODEWORD); + + state->got_second_code = true; +} + +/* + * We need different constants to make TCP run smoothly. TODO(Jfreegman): is this because of the group + * implementation or just an autotest quirk? + */ +#define GROUP_ITERATION_INTERVAL 100 +static void iterate_group(AutoTox *autotoxes, uint32_t num_toxes, size_t interval) +{ + for (uint32_t i = 0; i < num_toxes; i++) { + if (autotoxes[i].alive) { + tox_iterate(autotoxes[i].tox, &autotoxes[i]); + autotoxes[i].clock += interval; + } + } + + c_sleep(50); +} + +static bool all_peers_connected(AutoTox *autotoxes) +{ + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + + size_t count = 0; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + const State *state = (const State *)autotoxes[i].state; + + if (state->num_peers == NUM_GROUP_TOXES - 1) { + ++count; + } + } + + return count == NUM_GROUP_TOXES; +} + +static bool all_peers_got_code(AutoTox *autotoxes) +{ + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + + size_t count = 0; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + const State *state = (const State *)autotoxes[i].state; + + if (state->got_code) { + ++count; + } + } + + return count == NUM_GROUP_TOXES - 1; +} + +static void group_tcp_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert(NUM_GROUP_TOXES >= 2); + + State *state0 = (State *)autotoxes[0].state; + State *state1 = (State *)autotoxes[1].state; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_peer_join(autotoxes[i].tox, group_peer_join_handler); + tox_callback_group_private_message(autotoxes[i].tox, group_private_message_handler); + } + + tox_callback_group_message(autotoxes[1].tox, group_message_handler); + tox_callback_group_invite(autotoxes[1].tox, group_invite_handler); + + Tox_Err_Group_New new_err; + uint32_t groupnumber = tox_group_new(autotoxes[0].tox, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)"test", 4, + (const uint8_t *)"test", 4, &new_err); + ck_assert_msg(new_err == TOX_ERR_GROUP_NEW_OK, "tox_group_new failed: %d", new_err); + + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + + tox_group_get_chat_id(autotoxes[0].tox, groupnumber, chat_id, &id_err); + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "%d", id_err); + + printf("Tox 0 created new group...\n"); + + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + Tox_Err_Group_Join jerr; + tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"test", 4, nullptr, 0, &jerr); + ck_assert_msg(jerr == TOX_ERR_GROUP_JOIN_OK, "%d", jerr); + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL * 10); + } + + while (!all_peers_connected(autotoxes)) + ; + + printf("%d peers successfully joined. Waiting for code...\n", NUM_GROUP_TOXES); + printf("Tox 0 sending secret code to all peers\n"); + + + for (size_t i = 0; i < NUM_GROUP_TOXES - 1; ++i) { + + Tox_Err_Group_Send_Private_Message perr; + tox_group_send_private_message(autotoxes[0].tox, groupnumber, state0->peer_id[i], + TOX_MESSAGE_TYPE_NORMAL, + (const uint8_t *)CODEWORD, CODEWORD_LEN, &perr); + ck_assert_msg(perr == TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK, "%d", perr); + } + + while (!all_peers_got_code(autotoxes)) + ; + + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[1].tox, groupnumber, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + + state0->num_peers = 0; + state1->num_peers = 0; + + // now do a friend invite to make sure the TCP-specific logic for friend invites is okay + + printf("Tox1 leaves group and Tox0 does a friend group invite for tox1\n"); + + Tox_Err_Group_Invite_Friend err_invite; + tox_group_invite_friend(autotoxes[0].tox, groupnumber, 0, &err_invite); + ck_assert(err_invite == TOX_ERR_GROUP_INVITE_FRIEND_OK); + + while (state0->num_peers == 0 && state1->num_peers == 0) { + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + } + + printf("Tox 1 successfully joined. Waiting for code...\n"); + + Tox_Err_Group_Send_Message merr; + tox_group_send_message(autotoxes[0].tox, groupnumber, TOX_MESSAGE_TYPE_NORMAL, + (const uint8_t *)CODEWORD, CODEWORD_LEN, nullptr, &merr); + ck_assert(merr == TOX_ERR_GROUP_SEND_MESSAGE_OK); + + while (!state1->got_second_code) { + iterate_group(autotoxes, NUM_GROUP_TOXES, GROUP_ITERATION_INTERVAL); + } + + for (size_t i = 0; i < NUM_GROUP_TOXES; i++) { + tox_group_leave(autotoxes[i].tox, groupnumber, nullptr, 0, &err_exit); + ck_assert(err_exit == TOX_ERR_GROUP_LEAVE_OK); + } + + printf("Test passed!\n"); + +#endif // VANILLA_NACL +} +#endif // USE_TEST_NETWORK + +int main(void) +{ +#ifdef USE_TEST_NETWORK // TODO(Jfreegman): Enable this test when the mainnet works with DHT groupchats + setvbuf(stdout, nullptr, _IONBF, 0); + + struct Tox_Options *options = (struct Tox_Options *)calloc(1, sizeof(struct Tox_Options)); + ck_assert(options != nullptr); + + tox_options_default(options); + tox_options_set_udp_enabled(options, false); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(options, NUM_GROUP_TOXES, group_tcp_test, sizeof(State), &autotest_opts); + + tox_options_free(options); +#endif + return 0; +} + +#ifdef USE_TEST_NETWORK +#undef NUM_GROUP_TOXES +#undef CODEWORD_LEN +#undef CODEWORD +#endif // USE_TEST_NETWORK diff --git a/auto_tests/group_topic_test.c b/auto_tests/group_topic_test.c new file mode 100644 index 0000000000..26dafcb459 --- /dev/null +++ b/auto_tests/group_topic_test.c @@ -0,0 +1,345 @@ +/* + * Tests that we can successfully change the group topic, that all peers receive topic changes + * and that the topic lock works as intended. + */ + +#include +#include +#include +#include + +#include "auto_test_support.h" +#include "check_compat.h" + +#include "../toxcore/tox.h" +#include "../toxcore/group_chats.h" + +#define NUM_GROUP_TOXES 3 + +#define TOPIC "They're waiting for you Gordon...in the test chamber" +#define TOPIC_LEN (sizeof(TOPIC) - 1) + +#define TOPIC2 "They're waiting for you Gordon...in the test chamber 2.0" +#define TOPIC_LEN2 (sizeof(TOPIC2) - 1) + +#define GROUP_NAME "The Test Chamber" +#define GROUP_NAME_LEN (sizeof(GROUP_NAME) - 1) + +#define PEER0_NICK "Koresh" +#define PEER0_NICK_LEN (sizeof(PEER0_NICK) - 1) + +typedef struct State { + uint32_t peer_id; // the id of the peer we set to observer +} State; + +static bool all_group_peers_connected(const AutoTox *autotoxes, uint32_t tox_count, uint32_t groupnumber, + size_t name_length, uint32_t peer_limit) +{ + for (uint32_t i = 0; i < tox_count; ++i) { + // make sure we got an invite + if (tox_group_get_name_size(autotoxes[i].tox, groupnumber, nullptr) != name_length) { + return false; + } + + // make sure we got a sync response + if (peer_limit != 0 && tox_group_get_peer_limit(autotoxes[i].tox, groupnumber, nullptr) != peer_limit) { + return false; + } + + // make sure we're actually connected + if (!tox_group_is_connected(autotoxes[i].tox, groupnumber, nullptr)) { + return false; + } + } + + return true; +} + +static void group_peer_join_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, void *user_data) +{ + AutoTox *autotox = (AutoTox *)user_data; + ck_assert(autotox != nullptr); + + State *state = (State *)autotox->state; + + state->peer_id = peer_id; +} + +static void group_topic_handler(Tox *tox, uint32_t groupnumber, uint32_t peer_id, const uint8_t *topic, + size_t length, void *user_data) +{ + ck_assert(length <= TOX_GROUP_MAX_TOPIC_LENGTH); + + Tox_Err_Group_State_Queries query_err; + uint8_t topic2[TOX_GROUP_MAX_TOPIC_LENGTH]; + tox_group_get_topic(tox, groupnumber, topic2, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + size_t topic_length = tox_group_get_topic_size(tox, groupnumber, &query_err); + ck_assert(query_err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(topic_length == length && memcmp(topic, topic2, length) == 0, + "topic differs in callback: %s, %s", topic, topic2); +} + +static void group_topic_lock_handler(Tox *tox, uint32_t groupnumber, Tox_Group_Topic_Lock topic_lock, void *user_data) +{ + Tox_Err_Group_State_Queries err; + Tox_Group_Topic_Lock current_lock = tox_group_get_topic_lock(tox, groupnumber, &err); + + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + ck_assert_msg(topic_lock == current_lock, "topic locks differ in callback"); +} + +/* Sets group topic. + * + * Return true on success. + */ +static bool set_topic(Tox *tox, uint32_t groupnumber, const char *topic, size_t length) +{ + Tox_Err_Group_Topic_Set err; + tox_group_set_topic(tox, groupnumber, (const uint8_t *)topic, length, &err); + + return err == TOX_ERR_GROUP_TOPIC_SET_OK; +} + +/* Returns 0 if group topic matches expected topic. + * Returns a value < 0 on failure. + */ +static int check_topic(const Tox *tox, uint32_t groupnumber, const char *expected_topic, size_t expected_length) +{ + Tox_Err_Group_State_Queries query_err; + size_t topic_length = tox_group_get_topic_size(tox, groupnumber, &query_err); + + if (query_err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + return -1; + } + + if (expected_length != topic_length) { + return -2; + } + + uint8_t topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + tox_group_get_topic(tox, groupnumber, topic, &query_err); + + if (query_err != TOX_ERR_GROUP_STATE_QUERIES_OK) { + return -3; + } + + if (memcmp(expected_topic, (const char *)topic, topic_length) != 0) { + return -4; + } + + return 0; +} + +static void wait_topic_lock(AutoTox *autotoxes, uint32_t groupnumber, Tox_Group_Topic_Lock expected_lock) +{ + while (1) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + uint32_t count = 0; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + Tox_Err_Group_State_Queries err; + Tox_Group_Topic_Lock topic_lock = tox_group_get_topic_lock(autotoxes[i].tox, groupnumber, &err); + ck_assert(err == TOX_ERR_GROUP_STATE_QUERIES_OK); + + if (topic_lock == expected_lock) { + ++count; + } + } + + if (count == NUM_GROUP_TOXES) { + break; + } + } +} + +/* Waits for all peers in group to see the same topic */ +static void wait_state_topic(AutoTox *autotoxes, uint32_t groupnumber, const char *topic, size_t length) +{ + while (1) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + uint32_t count = 0; + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + const int c_ret = check_topic(autotoxes[i].tox, groupnumber, topic, length); + + if (c_ret == 0) { + ++count; + } + } + + if (count == NUM_GROUP_TOXES) { + break; + } + } +} + +/* All peers attempt to set the topic. + * + * Returns the number of peers who succeeeded. + */ +static uint32_t set_topic_all_peers(const Random *rng, AutoTox *autotoxes, size_t num_peers, uint32_t groupnumber) +{ + uint32_t change_count = 0; + + for (size_t i = 0; i < num_peers; ++i) { + char new_topic[TOX_GROUP_MAX_TOPIC_LENGTH]; + snprintf(new_topic, sizeof(new_topic), "peer %zu changes topic %u", i, random_u32(rng)); + size_t length = strlen(new_topic); + + if (set_topic(autotoxes[i].tox, groupnumber, new_topic, length)) { + wait_state_topic(autotoxes, groupnumber, new_topic, length); + ++change_count; + } else { + fprintf(stderr, "Peer %zu couldn't set the topic\n", i); + } + } + + return change_count; +} + +static void group_topic_test(AutoTox *autotoxes) +{ +#ifndef VANILLA_NACL + ck_assert_msg(NUM_GROUP_TOXES >= 3, "NUM_GROUP_TOXES is too small: %d", NUM_GROUP_TOXES); + + const Random *rng = system_random(); + ck_assert(rng != nullptr); + + Tox *tox0 = autotoxes[0].tox; + const State *state0 = (const State *)autotoxes[0].state; + + tox_callback_group_peer_join(tox0, group_peer_join_handler); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + tox_callback_group_topic(autotoxes[i].tox, group_topic_handler); + tox_callback_group_topic_lock(autotoxes[i].tox, group_topic_lock_handler); + } + + /* Tox1 creates a group and is the founder of a newly created group */ + Tox_Err_Group_New new_err; + const uint32_t groupnumber = tox_group_new(tox0, TOX_GROUP_PRIVACY_STATE_PUBLIC, (const uint8_t *)GROUP_NAME, + GROUP_NAME_LEN, + (const uint8_t *)PEER0_NICK, PEER0_NICK_LEN, &new_err); + + ck_assert_msg(new_err == TOX_ERR_GROUP_NEW_OK, "tox_group_new failed: %d", new_err); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + /* Founder sets group topic before anyone else joins */ + const bool s_ret = set_topic(tox0, groupnumber, TOPIC, TOPIC_LEN); + ck_assert_msg(s_ret, "Founder failed to set topic"); + + /* Founder gets the Chat ID and implicitly shares it publicly */ + Tox_Err_Group_State_Queries id_err; + uint8_t chat_id[TOX_GROUP_CHAT_ID_SIZE]; + tox_group_get_chat_id(tox0, groupnumber, chat_id, &id_err); + + ck_assert_msg(id_err == TOX_ERR_GROUP_STATE_QUERIES_OK, "tox_group_get_chat_id failed %d", id_err); + + /* All other peers join the group using the Chat ID */ + for (size_t i = 1; i < NUM_GROUP_TOXES; ++i) { + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + Tox_Err_Group_Join join_err; + tox_group_join(autotoxes[i].tox, chat_id, (const uint8_t *)"Test", 4, nullptr, 0, &join_err); + ck_assert_msg(join_err == TOX_ERR_GROUP_JOIN_OK, "tox_group_join failed: %d", join_err); + + c_sleep(100); + } + + fprintf(stderr, "Peers attempting to join group\n"); + + all_group_peers_connected(autotoxes, NUM_GROUP_TOXES, groupnumber, GROUP_NAME_LEN, MAX_GC_PEERS_DEFAULT); + + wait_state_topic(autotoxes, groupnumber, TOPIC, TOPIC_LEN); + + /* Founder disables topic lock */ + Tox_Err_Group_Founder_Set_Topic_Lock lock_set_err; + tox_group_founder_set_topic_lock(tox0, groupnumber, TOX_GROUP_TOPIC_LOCK_DISABLED, &lock_set_err); + ck_assert_msg(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, "failed to disable topic lock: %d", + lock_set_err); + + fprintf(stderr, "Topic lock disabled\n"); + + /* make sure every peer sees the topic lock state change */ + wait_topic_lock(autotoxes, groupnumber, TOX_GROUP_TOPIC_LOCK_DISABLED); + + /* All peers should be able to change the topic now */ + uint32_t change_count = set_topic_all_peers(rng, autotoxes, NUM_GROUP_TOXES, groupnumber); + + ck_assert_msg(change_count == NUM_GROUP_TOXES, "%u peers changed the topic with topic lock disabled", change_count); + + /* founder silences the last peer he saw join */ + Tox_Err_Group_Mod_Set_Role merr; + tox_group_mod_set_role(tox0, groupnumber, state0->peer_id, TOX_GROUP_ROLE_OBSERVER, &merr); + ck_assert_msg(merr == TOX_ERR_GROUP_MOD_SET_ROLE_OK, "Failed to set %u to observer role: %d", state0->peer_id, merr); + + fprintf(stderr, "Random peer is set to observer\n"); + + iterate_all_wait(autotoxes, NUM_GROUP_TOXES, ITERATION_INTERVAL); + + /* All peers except one should now be able to change the topic */ + change_count = set_topic_all_peers(rng, autotoxes, NUM_GROUP_TOXES, groupnumber); + + ck_assert_msg(change_count == NUM_GROUP_TOXES - 1, "%u peers changed the topic with a silenced peer", change_count); + + /* Founder enables topic lock and sets topic back to original */ + tox_group_founder_set_topic_lock(tox0, groupnumber, TOX_GROUP_TOPIC_LOCK_ENABLED, &lock_set_err); + ck_assert_msg(lock_set_err == TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, "failed to enable topic lock: %d", + lock_set_err); + + fprintf(stderr, "Topic lock enabled\n"); + + /* Wait for all peers to get topic lock state change */ + wait_topic_lock(autotoxes, groupnumber, TOX_GROUP_TOPIC_LOCK_ENABLED); + + const bool s3_ret = set_topic(tox0, groupnumber, TOPIC2, TOPIC_LEN2); + ck_assert_msg(s3_ret, "Founder failed to set topic second time"); + + wait_state_topic(autotoxes, groupnumber, TOPIC2, TOPIC_LEN2); + + /* No peer excluding the founder should be able to set the topic */ + + change_count = set_topic_all_peers(rng, &autotoxes[1], NUM_GROUP_TOXES - 1, groupnumber); + + ck_assert_msg(change_count == 0, "%u peers changed the topic with topic lock enabled", change_count); + + /* A final check that the topic is unchanged */ + wait_state_topic(autotoxes, groupnumber, TOPIC2, TOPIC_LEN2); + + for (size_t i = 0; i < NUM_GROUP_TOXES; ++i) { + Tox_Err_Group_Leave err_exit; + tox_group_leave(autotoxes[i].tox, groupnumber, nullptr, 0, &err_exit); + ck_assert_msg(err_exit == TOX_ERR_GROUP_LEAVE_OK, "%d", err_exit); + } + + fprintf(stderr, "All tests passed!\n"); + +#endif /* VANILLA_NACL */ +} + +int main(void) +{ + setvbuf(stdout, nullptr, _IONBF, 0); + + Run_Auto_Options autotest_opts = default_run_auto_options(); + autotest_opts.graph = GRAPH_COMPLETE; + + run_auto_test(nullptr, NUM_GROUP_TOXES, group_topic_test, sizeof(State), &autotest_opts); + + return 0; +} + +#undef TOPIC +#undef TOPIC_LEN +#undef TOPIC2 +#undef TOPIC_LEN2 +#undef NUM_GROUP_TOXES +#undef GROUP_NAME +#undef GROUP_NAME_LEN +#undef PEER0_NICK +#undef PEER0_NICK_LEN diff --git a/auto_tests/onion_test.c b/auto_tests/onion_test.c index 36fcc202a8..02a15b2247 100644 --- a/auto_tests/onion_test.c +++ b/auto_tests/onion_test.c @@ -41,7 +41,7 @@ static int handle_test_1(void *object, const IP_Port *source, const uint8_t *pac const char req_message[] = "Install Gentoo"; uint8_t req_packet[1 + sizeof(req_message)]; - req_packet[0] = NET_PACKET_ANNOUNCE_REQUEST_OLD; + req_packet[0] = NET_PACKET_ANNOUNCE_REQUEST; memcpy(req_packet + 1, req_message, sizeof(req_message)); if (memcmp(packet, req_packet, sizeof(req_packet)) != 0) { @@ -50,7 +50,7 @@ static int handle_test_1(void *object, const IP_Port *source, const uint8_t *pac const char res_message[] = "install gentoo"; uint8_t res_packet[1 + sizeof(res_message)]; - res_packet[0] = NET_PACKET_ANNOUNCE_RESPONSE_OLD; + res_packet[0] = NET_PACKET_ANNOUNCE_RESPONSE; memcpy(res_packet + 1, res_message, sizeof(res_message)); if (send_onion_response(onion->net, source, res_packet, sizeof(res_packet), @@ -67,7 +67,7 @@ static int handle_test_2(void *object, const IP_Port *source, const uint8_t *pac { const char res_message[] = "install gentoo"; uint8_t res_packet[1 + sizeof(res_message)]; - res_packet[0] = NET_PACKET_ANNOUNCE_RESPONSE_OLD; + res_packet[0] = NET_PACKET_ANNOUNCE_RESPONSE; memcpy(res_packet + 1, res_message, sizeof(res_message)); if (length != sizeof(res_packet)) { @@ -101,12 +101,47 @@ static int handle_test_3(void *object, const IP_Port *source, const uint8_t *pac { Onion *onion = (Onion *)object; - if (length != (1 + CRYPTO_NONCE_SIZE + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + 1 + CRYPTO_SHA256_SIZE + - CRYPTO_MAC_SIZE)) { + if (length < ONION_ANNOUNCE_RESPONSE_MIN_SIZE || length > ONION_ANNOUNCE_RESPONSE_MAX_SIZE) { return 1; } - uint8_t plain[1 + CRYPTO_SHA256_SIZE]; + uint8_t plain[2 + CRYPTO_SHA256_SIZE]; +#if 0 + print_client_id(packet, length); +#endif + int len = decrypt_data(test_3_pub_key, dht_get_self_secret_key(onion->dht), + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, + 2 + CRYPTO_SHA256_SIZE + CRYPTO_MAC_SIZE, plain); + + if (len == -1) { + return 1; + } + + + if (memcmp(packet + 1, sb_data, ONION_ANNOUNCE_SENDBACK_DATA_LENGTH) != 0) { + return 1; + } + + memcpy(test_3_ping_id, plain + 1, CRYPTO_SHA256_SIZE); +#if 0 + print_client_id(test_3_ping_id, sizeof(test_3_ping_id)); +#endif + handled_test_3 = 1; + return 0; +} + +/* TODO: DEPRECATE */ +static int handle_test_3_old(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, + void *userdata) +{ + Onion *onion = (Onion *)object; + + if (length < ONION_ANNOUNCE_RESPONSE_MIN_SIZE || length > ONION_ANNOUNCE_RESPONSE_MAX_SIZE) { + return 1; + } + + uint8_t plain[2 + CRYPTO_SHA256_SIZE]; #if 0 print_client_id(packet, length); #endif @@ -204,7 +239,7 @@ static void test_basic(void) Onion *onion1 = new_onion(log1, mono_time1, rng, new_dht(log1, rng, ns, mono_time1, new_networking(log1, ns, &ip, 36567), true, false)); Onion *onion2 = new_onion(log2, mono_time2, rng, new_dht(log2, rng, ns, mono_time2, new_networking(log2, ns, &ip, 36568), true, false)); ck_assert_msg((onion1 != nullptr) && (onion2 != nullptr), "Onion failed initializing."); - networking_registerhandler(onion2->net, NET_PACKET_ANNOUNCE_REQUEST_OLD, &handle_test_1, onion2); + networking_registerhandler(onion2->net, NET_PACKET_ANNOUNCE_REQUEST, &handle_test_1, onion2); IP_Port on1 = {ip, net_port(onion1->net)}; Node_format n1; @@ -218,7 +253,7 @@ static void test_basic(void) const char req_message[] = "Install Gentoo"; uint8_t req_packet[1 + sizeof(req_message)]; - req_packet[0] = NET_PACKET_ANNOUNCE_REQUEST_OLD; + req_packet[0] = NET_PACKET_ANNOUNCE_REQUEST; memcpy(req_packet + 1, req_message, sizeof(req_message)); Node_format nodes[4]; @@ -237,7 +272,7 @@ static void test_basic(void) do_onion(mono_time2, onion2); } while (handled_test_1 == 0); - networking_registerhandler(onion1->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_test_2, onion1); + networking_registerhandler(onion1->net, NET_PACKET_ANNOUNCE_RESPONSE, &handle_test_2, onion1); handled_test_2 = 0; do { @@ -247,7 +282,8 @@ static void test_basic(void) Onion_Announce *onion1_a = new_onion_announce(log1, rng, mono_time1, onion1->dht); Onion_Announce *onion2_a = new_onion_announce(log2, rng, mono_time2, onion2->dht); - networking_registerhandler(onion1->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_test_3, onion1); + networking_registerhandler(onion1->net, NET_PACKET_ANNOUNCE_RESPONSE, &handle_test_3, onion1); + networking_registerhandler(onion1->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_test_3_old, onion1); ck_assert_msg((onion1_a != nullptr) && (onion2_a != nullptr), "Onion_Announce failed initializing."); uint8_t zeroes[64] = {0}; random_bytes(rng, sb_data, sizeof(sb_data)); @@ -269,6 +305,8 @@ static void test_basic(void) c_sleep(50); } while (handled_test_3 == 0); + printf("test 3 complete\n"); + random_bytes(rng, sb_data, sizeof(sb_data)); memcpy(&s, sb_data, sizeof(uint64_t)); memcpy(onion_announce_entry_public_key(onion2_a, 1), dht_get_self_public_key(onion2->dht), CRYPTO_PUBLIC_KEY_SIZE); @@ -312,6 +350,8 @@ static void test_basic(void) c_sleep(50); } while (handled_test_4 == 0); + printf("test 4 complete\n"); + kill_onion_announce(onion2_a); kill_onion_announce(onion1_a); diff --git a/configure.ac b/configure.ac index 8c69003c76..1afb0a7585 100644 --- a/configure.ac +++ b/configure.ac @@ -171,6 +171,16 @@ if test "$use_ipv6" != "yes"; then AC_DEFINE([USE_IPV6],[0],[define to 0 to force ipv4]) fi +AC_ARG_ENABLE([[test_network]], + [AS_HELP_STRING([[--enable-test-network[=ARG]]], [build tox for a test network incompatible with the main DHT [no]])], + [use_test_network=${enableval}], + [use_test_network='no'] + ) + +if test "$use_test_network" == "yes"; then + AC_DEFINE([USE_TEST_NETWORK],[1],[define to 1 to enable the test network]) +fi + AX_HAVE_EPOLL if test "$enable_epoll" != "no"; then if test "${ax_cv_have_epoll}" = "yes"; then diff --git a/other/BUILD.bazel b/other/BUILD.bazel index da8573a289..1a8929a375 100644 --- a/other/BUILD.bazel +++ b/other/BUILD.bazel @@ -19,11 +19,15 @@ cc_binary( "//c-toxcore/testing:misc_tools", "//c-toxcore/toxcore:DHT", "//c-toxcore/toxcore:LAN_discovery", + "//c-toxcore/toxcore:Messenger", "//c-toxcore/toxcore:TCP_server", "//c-toxcore/toxcore:ccompat", "//c-toxcore/toxcore:friend_requests", + "//c-toxcore/toxcore:group_onion_announce", "//c-toxcore/toxcore:logger", "//c-toxcore/toxcore:mono_time", + "//c-toxcore/toxcore:network", + "//c-toxcore/toxcore:onion_announce", "//c-toxcore/toxcore:tox", "//c-toxcore/toxcore:util", ], diff --git a/other/DHT_bootstrap.c b/other/DHT_bootstrap.c index 56796adabf..4128992e4a 100644 --- a/other/DHT_bootstrap.c +++ b/other/DHT_bootstrap.c @@ -17,6 +17,7 @@ #include "../toxcore/LAN_discovery.h" #include "../toxcore/ccompat.h" #include "../toxcore/friend_requests.h" +#include "../toxcore/group_onion_announce.h" #include "../toxcore/logger.h" #include "../toxcore/mono_time.h" #include "../toxcore/tox.h" @@ -150,7 +151,8 @@ int main(int argc, char *argv[]) DHT *dht = new_dht(logger, rng, ns, mono_time, new_networking_ex(logger, ns, &ip, start_port, end_port, nullptr), true, true); Onion *onion = new_onion(logger, mono_time, rng, dht); Forwarding *forwarding = new_forwarding(logger, rng, mono_time, dht); - const Onion_Announce *onion_a = new_onion_announce(logger, rng, mono_time, dht); + GC_Announces_List *gc_announces_list = new_gca_list(); + Onion_Announce *onion_a = new_onion_announce(logger, rng, mono_time, dht); #ifdef DHT_NODE_EXTRA_PACKETS bootstrap_set_callbacks(dht_get_net(dht), DHT_VERSION_NUMBER, DHT_MOTD, sizeof(DHT_MOTD)); @@ -161,6 +163,8 @@ int main(int argc, char *argv[]) exit(1); } + gca_onion_init(gc_announces_list, onion_a); + perror("Initialization"); manage_keys(dht); diff --git a/other/analysis/variants.sh b/other/analysis/variants.sh index ca14b64ea6..6a8241bae9 100644 --- a/other/analysis/variants.sh +++ b/other/analysis/variants.sh @@ -1,4 +1,3 @@ #!/bin/bash -run "$@" -run -DVANILLA_NACL -I/usr/include/sodium "$@" +run diff --git a/other/bootstrap_daemon/BUILD.bazel b/other/bootstrap_daemon/BUILD.bazel index 395459245e..e1a2e41f44 100644 --- a/other/bootstrap_daemon/BUILD.bazel +++ b/other/bootstrap_daemon/BUILD.bazel @@ -15,6 +15,7 @@ cc_binary( "//c-toxcore/toxcore:TCP_server", "//c-toxcore/toxcore:announce", "//c-toxcore/toxcore:ccompat", + "//c-toxcore/toxcore:group_onion_announce", "//c-toxcore/toxcore:logger", "//c-toxcore/toxcore:mono_time", "//c-toxcore/toxcore:onion_announce", diff --git a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 index 79af32da48..8b38a0db64 100644 --- a/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 +++ b/other/bootstrap_daemon/docker/tox-bootstrapd.sha256 @@ -1 +1 @@ -d2f8e02aeb249b0a22d4a7dee087d5f1301385587cb528e9ae055a6ceb2c4567 /usr/local/bin/tox-bootstrapd +ed8c226b4a7a95dacfb6170d351ff5e7440a848749e9d0527f85ea7f7a6d3924 /usr/local/bin/tox-bootstrapd diff --git a/other/bootstrap_daemon/src/tox-bootstrapd.c b/other/bootstrap_daemon/src/tox-bootstrapd.c index 735bf0c278..d12caf9bd2 100644 --- a/other/bootstrap_daemon/src/tox-bootstrapd.c +++ b/other/bootstrap_daemon/src/tox-bootstrapd.c @@ -29,6 +29,7 @@ #include "../../../toxcore/LAN_discovery.h" #include "../../../toxcore/TCP_server.h" #include "../../../toxcore/announce.h" +#include "../../../toxcore/group_onion_announce.h" #include "../../../toxcore/logger.h" #include "../../../toxcore/mono_time.h" #include "../../../toxcore/onion_announce.h" @@ -364,6 +365,22 @@ int main(int argc, char *argv[]) return 1; } + GC_Announces_List *group_announce = new_gca_list(); + + if (group_announce == nullptr) { + log_write(LOG_LEVEL_ERROR, "Couldn't initialize group announces. Exiting.\n"); + kill_announcements(announce); + kill_forwarding(forwarding); + kill_dht(dht); + mono_time_free(mono_time); + kill_networking(net); + logger_kill(logger); + free(motd); + free(tcp_relay_ports); + free(keys_file_path); + return 1; + } + Onion *onion = new_onion(logger, mono_time, rng, dht); if (!onion) { @@ -384,6 +401,7 @@ int main(int argc, char *argv[]) if (!onion_a) { log_write(LOG_LEVEL_ERROR, "Couldn't initialize Tox Onion Announce. Exiting.\n"); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); @@ -397,6 +415,8 @@ int main(int argc, char *argv[]) return 1; } + gca_onion_init(group_announce, onion_a); + if (enable_motd) { if (bootstrap_set_callbacks(dht_get_net(dht), DAEMON_VERSION_NUMBER, (uint8_t *)motd, strlen(motd) + 1) == 0) { log_write(LOG_LEVEL_INFO, "Set MOTD successfully.\n"); @@ -404,6 +424,7 @@ int main(int argc, char *argv[]) } else { log_write(LOG_LEVEL_ERROR, "Couldn't set MOTD: %s. Exiting.\n", motd); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); @@ -424,6 +445,7 @@ int main(int argc, char *argv[]) } else { log_write(LOG_LEVEL_ERROR, "Couldn't read/write: %s. Exiting.\n", keys_file_path); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); @@ -442,6 +464,7 @@ int main(int argc, char *argv[]) if (tcp_relay_port_count == 0) { log_write(LOG_LEVEL_ERROR, "No TCP relay ports read. Exiting.\n"); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_announcements(announce); kill_forwarding(forwarding); kill_onion(onion); @@ -487,6 +510,7 @@ int main(int argc, char *argv[]) } else { log_write(LOG_LEVEL_ERROR, "Couldn't initialize Tox TCP server. Exiting.\n"); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); @@ -504,6 +528,7 @@ int main(int argc, char *argv[]) log_write(LOG_LEVEL_ERROR, "Couldn't read list of bootstrap nodes in %s. Exiting.\n", cfg_file_path); kill_TCP_server(tcp_server); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); @@ -586,6 +611,7 @@ int main(int argc, char *argv[]) lan_discovery_kill(broadcast); kill_TCP_server(tcp_server); kill_onion_announce(onion_a); + kill_gca(group_announce); kill_onion(onion); kill_announcements(announce); kill_forwarding(forwarding); diff --git a/testing/misc_tools.h b/testing/misc_tools.h index 92fc954783..3e5627d5e3 100644 --- a/testing/misc_tools.h +++ b/testing/misc_tools.h @@ -13,6 +13,7 @@ extern "C" { void c_sleep(uint32_t x); uint8_t *hex_string_to_bin(const char *hex_string); +char *id_toa(const uint8_t *id); void to_hex(char *out, uint8_t *in, int size); int tox_strncasecmp(const char *s1, const char *s2, size_t n); int cmdline_parsefor_ipv46(int argc, char **argv, bool *ipv6enabled); diff --git a/third_party/.gitignore b/third_party/.gitignore new file mode 100644 index 0000000000..8e2118051f --- /dev/null +++ b/third_party/.gitignore @@ -0,0 +1 @@ +googletest diff --git a/toxcore/BUILD.bazel b/toxcore/BUILD.bazel index c8258f72cf..489f810992 100644 --- a/toxcore/BUILD.bazel +++ b/toxcore/BUILD.bazel @@ -471,6 +471,7 @@ cc_library( hdrs = ["onion_announce.h"], visibility = [ "//c-toxcore/auto_tests:__pkg__", + "//c-toxcore/other:__pkg__", "//c-toxcore/other/bootstrap_daemon:__pkg__", ], deps = [ @@ -514,6 +515,22 @@ cc_test( ], ) +cc_library( + name = "group_onion_announce", + srcs = ["group_onion_announce.c"], + hdrs = ["group_onion_announce.h"], + visibility = [ + "//c-toxcore/auto_tests:__pkg__", + "//c-toxcore/other:__pkg__", + "//c-toxcore/other/bootstrap_daemon:__pkg__", + ], + deps = [ + ":ccompat", + ":group_announce", + ":onion_announce", + ], +) + cc_fuzz_test( name = "group_announce_fuzz_test", srcs = ["group_announce_fuzz_test.cc"], @@ -533,8 +550,10 @@ cc_library( ":DHT", ":LAN_discovery", ":ccompat", + ":group_onion_announce", ":mono_time", ":net_crypto", + ":network", ":onion_announce", ":util", ], @@ -578,6 +597,7 @@ cc_library( deps = [ ":ccompat", ":friend_connection", + ":network", ":util", ], ) @@ -624,24 +644,48 @@ cc_fuzz_test( cc_library( name = "Messenger", - srcs = ["Messenger.c"], - hdrs = ["Messenger.h"], + srcs = [ + "Messenger.c", + "group_chats.c", + "group_connection.c", + "group_pack.c", + ], + hdrs = [ + "Messenger.h", + "group_chats.h", + "group_common.h", + "group_connection.h", + "group_pack.h", + ], visibility = [ "//c-toxcore/auto_tests:__pkg__", + "//c-toxcore/other:__pkg__", "//c-toxcore/testing:__pkg__", "//c-toxcore/toxav:__pkg__", ], deps = [ + ":DHT", + ":LAN_discovery", + ":TCP_connection", ":TCP_server", ":announce", + ":bin_pack", + ":bin_unpack", ":ccompat", + ":crypto_core", ":forwarding", + ":friend_connection", ":friend_requests", + ":group_moderation", + ":group_onion_announce", ":logger", ":mono_time", + ":net_crypto", ":network", + ":onion_announce", ":state", ":util", + "@libsodium", ], ) @@ -676,6 +720,7 @@ cc_library( ":Messenger", ":ccompat", ":group", + ":group_moderation", ":logger", ":mono_time", ":network", diff --git a/toxcore/DHT.h b/toxcore/DHT.h index 3fc5c87bec..ee6470ab9e 100644 --- a/toxcore/DHT.h +++ b/toxcore/DHT.h @@ -22,6 +22,20 @@ extern "C" { #endif +/* Encryption and signature keys definition */ +#define ENC_PUBLIC_KEY_SIZE CRYPTO_PUBLIC_KEY_SIZE +#define ENC_SECRET_KEY_SIZE CRYPTO_SECRET_KEY_SIZE +#define SIG_PUBLIC_KEY_SIZE CRYPTO_SIGN_PUBLIC_KEY_SIZE +#define SIG_SECRET_KEY_SIZE CRYPTO_SIGN_SECRET_KEY_SIZE + +/* Size of the group chat_id */ +#define CHAT_ID_SIZE SIG_PUBLIC_KEY_SIZE + +/* Extended keys for group chats */ +#define EXT_SECRET_KEY_SIZE (ENC_SECRET_KEY_SIZE + SIG_SECRET_KEY_SIZE) +#define EXT_PUBLIC_KEY_SIZE (ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE) + + /* Maximum size of a signature (may be smaller) */ #define SIGNATURE_SIZE CRYPTO_SIGNATURE_SIZE /** Maximum number of clients stored per friend. */ diff --git a/toxcore/LAN_discovery.c b/toxcore/LAN_discovery.c index 82c02b59e9..ef44d3b552 100644 --- a/toxcore/LAN_discovery.c +++ b/toxcore/LAN_discovery.c @@ -339,7 +339,8 @@ bool ip_is_lan(const IP *ip) } -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port) +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, + uint16_t port) { if (broadcast == nullptr) { return false; diff --git a/toxcore/LAN_discovery.h b/toxcore/LAN_discovery.h index 85ea9478f4..5d9c83335b 100644 --- a/toxcore/LAN_discovery.h +++ b/toxcore/LAN_discovery.h @@ -24,7 +24,8 @@ typedef struct Broadcast_Info Broadcast_Info; * @return true on success, false on failure. */ non_null() -bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, uint16_t port); +bool lan_discovery_send(const Networking_Core *net, const Broadcast_Info *broadcast, const uint8_t *dht_pk, + uint16_t port); /** * Discovers broadcast devices and IP addresses. diff --git a/toxcore/Makefile.inc b/toxcore/Makefile.inc index 239154f6d8..c7a01dc309 100644 --- a/toxcore/Makefile.inc +++ b/toxcore/Makefile.inc @@ -79,6 +79,19 @@ libtoxcore_la_SOURCES = ../third_party/cmp/cmp.c \ ../toxcore/util.c \ ../toxcore/group.h \ ../toxcore/group.c \ + ../toxcore/group_announce.h \ + ../toxcore/group_announce.c \ + ../toxcore/group_onion_announce.c \ + ../toxcore/group_onion_announce.h \ + ../toxcore/group_chats.h \ + ../toxcore/group_chats.c \ + ../toxcore/group_common.h \ + ../toxcore/group_connection.c \ + ../toxcore/group_connection.h \ + ../toxcore/group_pack.c \ + ../toxcore/group_pack.h \ + ../toxcore/group_moderation.c \ + ../toxcore/group_moderation.h \ ../toxcore/onion.h \ ../toxcore/onion.c \ ../toxcore/logger.h \ diff --git a/toxcore/Messenger.c b/toxcore/Messenger.c index 96aeaa62cd..8c5ae36ced 100644 --- a/toxcore/Messenger.c +++ b/toxcore/Messenger.c @@ -14,7 +14,10 @@ #include #include +#include "DHT.h" #include "ccompat.h" +#include "group_chats.h" +#include "group_onion_announce.h" #include "logger.h" #include "mono_time.h" #include "network.h" @@ -26,6 +29,16 @@ static_assert(MAX_CONCURRENT_FILE_PIPES <= UINT8_MAX + 1, static const Friend empty_friend = {{0}}; +/** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +bool friend_is_valid(const Messenger *m, int32_t friendnumber) +{ + return (uint32_t)friendnumber < m->numfriends && m->friendlist[friendnumber].status != 0; +} + /** @brief Set the size of the friend list to numfriends. * * @retval -1 if realloc fails. @@ -107,15 +120,11 @@ void getaddress(const Messenger *m, uint8_t *address) } non_null() -static bool send_online_packet(Messenger *m, int32_t friendnumber) +static bool send_online_packet(Messenger *m, int friendcon_id) { - if (!m_friend_exists(m, friendnumber)) { - return false; - } - uint8_t packet = PACKET_ID_ONLINE; - return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, - m->friendlist[friendnumber].friendcon_id), &packet, sizeof(packet), false) != -1; + return write_cryptpacket(m->net_crypto, friend_connection_crypt_connection_id(m->fr_c, friendcon_id), &packet, + sizeof(packet), false) != -1; } non_null() @@ -174,7 +183,7 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta } if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { - send_online_packet(m, i); + send_online_packet(m, friendcon_id); } return i; @@ -184,6 +193,20 @@ static int32_t init_new_friend(Messenger *m, const uint8_t *real_pk, uint8_t sta return FAERR_NOMEM; } +non_null() +static int32_t m_add_friend_contact_norequest(Messenger *m, const uint8_t *real_pk) +{ + if (getfriend_id(m, real_pk) != -1) { + return FAERR_ALREADYSENT; + } + + if (pk_equal(real_pk, nc_get_self_public_key(m->net_crypto))) { + return FAERR_OWNKEY; + } + + return init_new_friend(m, real_pk, FRIEND_CONFIRMED); +} + /** * Add a friend. * @@ -268,10 +291,6 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk) { - if (getfriend_id(m, real_pk) != -1) { - return FAERR_ALREADYSENT; - } - if (!public_key_valid(real_pk)) { return FAERR_BADCHECKSUM; } @@ -280,7 +299,7 @@ int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk) return FAERR_OWNKEY; } - return init_new_friend(m, real_pk, FRIEND_CONFIRMED); + return m_add_friend_contact_norequest(m, real_pk); } non_null() @@ -344,6 +363,53 @@ static int friend_received_packet(const Messenger *m, int32_t friendnumber, uint m->friendlist[friendnumber].friendcon_id), number); } +bool m_create_group_connection(Messenger *m, GC_Chat *chat) +{ + random_bytes(m->rng, chat->m_group_public_key, CRYPTO_PUBLIC_KEY_SIZE); + const int friendcon_id = new_friend_connection(m->fr_c, chat->m_group_public_key); + + if (friendcon_id == -1) { + return false; + } + + const Friend_Conn *connection = get_conn(m->fr_c, friendcon_id); + + if (connection == nullptr) { + return false; + } + + chat->friend_connection_id = friendcon_id; + + if (friend_con_connected(m->fr_c, friendcon_id) == FRIENDCONN_STATUS_CONNECTED) { + send_online_packet(m, friendcon_id); + } + + const int onion_friend_number = friend_conn_get_onion_friendnum(connection); + Onion_Friend *onion_friend = onion_get_friend(m->onion_c, (uint16_t)onion_friend_number); + + onion_friend_set_gc_public_key(onion_friend, get_chat_id(chat->chat_public_key)); + onion_friend_set_gc_data(onion_friend, nullptr, 0); + + return true; +} + +/** + * Kills the friend connection for a groupchat. + */ +void m_kill_group_connection(Messenger *m, const GC_Chat *chat) +{ + remove_request_received(m->fr, chat->m_group_public_key); + + friend_connection_callbacks(m->fr_c, chat->friend_connection_id, MESSENGER_CALLBACK_INDEX, nullptr, + nullptr, nullptr, nullptr, 0); + + if (friend_con_connected(m->fr_c, chat->friend_connection_id) == FRIENDCONN_STATUS_CONNECTED) { + send_offline_packet(m, chat->friend_connection_id); + } + + kill_friend_connection(m->fr_c, chat->friend_connection_id); +} + non_null(1) nullable(3) static int do_receipts(Messenger *m, int32_t friendnumber, void *userdata) { @@ -991,6 +1057,11 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function m->conference_invite = function; } +/** @brief the callback for group invites. */ +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function) +{ + m->group_invite = function; +} /** @brief Send a conference invite packet. * @@ -1002,6 +1073,17 @@ bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, con return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_CONFERENCE, data, length, false); } + +/** @brief Send a group invite packet. + * + * @retval true if success + */ +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length) +{ + return write_cryptpacket_id(m, friendnumber, PACKET_ID_INVITE_GROUPCHAT, data, length, false); +} + + /*** FILE SENDING */ @@ -1837,7 +1919,7 @@ int m_send_custom_lossy_packet(const Messenger *m, int32_t friendnumber, const u } non_null(1, 3) nullable(5) -static int m_handle_custom_lossless_packet(void *object, int friend_num, const uint8_t *packet, uint16_t length, +static int handle_custom_lossless_packet(void *object, int friend_num, const uint8_t *packet, uint16_t length, void *userdata) { Messenger *m = (Messenger *)object; @@ -1854,7 +1936,7 @@ static int m_handle_custom_lossless_packet(void *object, int friend_num, const u m->lossless_packethandler(m, friend_num, packet[0], packet, length, userdata); } - return 0; + return 1; } void custom_lossless_packet_registerhandler(Messenger *m, m_friend_lossless_packet_cb *lossless_packethandler) @@ -1928,7 +2010,7 @@ static int m_handle_status(void *object, int i, bool status, void *userdata) Messenger *m = (Messenger *)object; if (status) { /* Went online. */ - send_online_packet(m, i); + send_online_packet(m, m->friendlist[i].friendcon_id); } else { /* Went offline. */ if (m->friendlist[i].status == FRIEND_ONLINE) { set_friend_status(m, i, FRIEND_CONFIRMED, userdata); @@ -2244,6 +2326,36 @@ static int m_handle_packet_msi(Messenger *m, const int i, const uint8_t *data, c return 0; } +non_null(1, 3) nullable(5) +static int m_handle_packet_invite_groupchat(Messenger *m, const int i, const uint8_t *data, const uint16_t data_length, void *userdata) +{ +#ifndef VANILLA_NACL + + // first two bytes are messenger packet type and group invite type + if (data_length < 2 + GC_JOIN_DATA_LENGTH) { + return 0; + } + + const uint8_t invite_type = data[1]; + const uint8_t *join_data = data + 2; + const uint32_t join_data_len = data_length - 2; + + if (m->group_invite != nullptr && data[1] == GROUP_INVITE && data_length != 2 + GC_JOIN_DATA_LENGTH) { + if (group_not_added(m->group_handler, join_data, join_data_len)) { + m->group_invite(m, i, join_data, GC_JOIN_DATA_LENGTH, + join_data + GC_JOIN_DATA_LENGTH, join_data_len - GC_JOIN_DATA_LENGTH, userdata); + } + } else if (invite_type == GROUP_INVITE_ACCEPTED) { + handle_gc_invite_accepted_packet(m->group_handler, i, join_data, join_data_len); + } else if (invite_type == GROUP_INVITE_CONFIRMATION) { + handle_gc_invite_confirmed_packet(m->group_handler, i, join_data, join_data_len); + } + +#endif // VANILLA_NACL + + return 0; +} + non_null(1, 3) nullable(5) static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t len, void *userdata) { @@ -2259,7 +2371,7 @@ static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t le if (m->friendlist[i].status != FRIEND_ONLINE) { if (packet_id == PACKET_ID_ONLINE && len == 1) { set_friend_status(m, i, FRIEND_ONLINE, userdata); - send_online_packet(m, i); + send_online_packet(m, m->friendlist[i].friendcon_id); } else { return -1; } @@ -2291,9 +2403,11 @@ static int m_handle_packet(void *object, int i, const uint8_t *temp, uint16_t le return m_handle_packet_file_data(m, i, data, data_length, userdata); case PACKET_ID_MSI: return m_handle_packet_msi(m, i, data, data_length, userdata); + case PACKET_ID_INVITE_GROUPCHAT: + return m_handle_packet_invite_groupchat(m, i, data, data_length, userdata); } - return m_handle_custom_lossless_packet(object, i, temp, len, userdata); + return handle_custom_lossless_packet(object, i, temp, len, userdata); } non_null(1) nullable(2) @@ -2414,6 +2528,82 @@ uint32_t messenger_run_interval(const Messenger *m) return crypto_interval; } +/** @brief Attempts to create a DHT announcement for a group chat with our connection info. An + * announcement can only be created if we either have a UDP or TCP connection to the network. + * + * @retval true if success. + */ +#ifndef VANILLA_NACL +non_null() +static bool self_announce_group(const Messenger *m, GC_Chat *chat, Onion_Friend *onion_friend) +{ + GC_Public_Announce announce = {{{{{0}}}}}; + + const bool ip_port_is_set = chat->self_udp_status != SELF_UDP_STATUS_NONE; + const int tcp_num = tcp_copy_connected_relays(chat->tcp_conn, announce.base_announce.tcp_relays, + GCA_MAX_ANNOUNCED_TCP_RELAYS); + + if (tcp_num == 0 && !ip_port_is_set) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + announce.base_announce.tcp_relays_count = (uint8_t)tcp_num; + announce.base_announce.ip_port_is_set = (uint8_t)(ip_port_is_set ? 1 : 0); + + if (ip_port_is_set) { + memcpy(&announce.base_announce.ip_port, &chat->self_ip_port, sizeof(IP_Port)); + } + + memcpy(announce.base_announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + memcpy(announce.chat_public_key, get_chat_id(chat->chat_public_key), ENC_PUBLIC_KEY_SIZE); + + uint8_t gc_data[GCA_MAX_DATA_LENGTH]; + const int length = gca_pack_public_announce(m->log, gc_data, GCA_MAX_DATA_LENGTH, &announce); + + if (length <= 0) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + if (gca_add_announce(m->mono_time, m->group_announce, &announce) == nullptr) { + onion_friend_set_gc_data(onion_friend, nullptr, 0); + return false; + } + + onion_friend_set_gc_data(onion_friend, gc_data, (uint16_t)length); + chat->update_self_announces = false; + + LOGGER_DEBUG(chat->log, "Published group announce. TCP relays: %d, UDP status: %d", tcp_num, + chat->self_udp_status); + return true; +} + +non_null() +static void do_gc_onion_friends(const Messenger *m) +{ + const uint16_t num_friends = onion_get_friend_count(m->onion_c); + + for (uint16_t i = 0; i < num_friends; ++i) { + Onion_Friend *onion_friend = onion_get_friend(m->onion_c, i); + + if (!onion_friend_is_groupchat(onion_friend)) { + continue; + } + + GC_Chat *chat = gc_get_group_by_public_key(m->group_handler, onion_friend_get_gc_public_key(onion_friend)); + + if (chat == nullptr) { + continue; + } + + if (chat->update_self_announces) { + self_announce_group(m, chat, onion_friend); + } + } +} +#endif // VANILLA_NACL + /** @brief The main loop that needs to be run at least 20 times per second. */ void do_messenger(Messenger *m, void *userdata) { @@ -2450,6 +2640,11 @@ void do_messenger(Messenger *m, void *userdata) do_onion_client(m->onion_c); do_friend_connections(m->fr_c, userdata); do_friends(m, userdata); +#ifndef VANILLA_NACL + do_gc(m->group_handler, userdata); + do_gca(m->mono_time, m->group_announce); + do_gc_onion_friends(m); +#endif m_connection_status_callback(m, userdata); if (mono_time_get(m->mono_time) > m->lastdump + DUMPING_CLIENTS_FRIENDS_EVERY_N_SECONDS) { @@ -2930,6 +3125,101 @@ static State_Load_Status friends_list_load(Messenger *m, const uint8_t *data, ui return STATE_LOAD_STATUS_CONTINUE; } +#ifndef VANILLA_NACL +non_null() +static void pack_groupchats(const GC_Session *c, Bin_Pack *bp) +{ + assert(bp != nullptr && c != nullptr); + bin_pack_array(bp, gc_count_groups(c)); + + for (uint32_t i = 0; i < c->chats_index; ++i) { // this loop must match the one in gc_count_groups() + const GC_Chat *chat = &c->chats[i]; + + if (!gc_group_is_valid(chat)) { + continue; + } + + gc_group_save(chat, bp); + } +} + +non_null() +static bool pack_groupchats_handler(Bin_Pack *bp, const void *obj) +{ + pack_groupchats((const GC_Session *)obj, bp); + return true; // TODO(iphydf): Return bool from pack functions. +} + +non_null() +static uint32_t saved_groups_size(const Messenger *m) +{ + GC_Session *c = m->group_handler; + return bin_pack_obj_size(pack_groupchats_handler, c); +} + +non_null() +static uint8_t *groups_save(const Messenger *m, uint8_t *data) +{ + const GC_Session *c = m->group_handler; + + const uint32_t num_groups = gc_count_groups(c); + + if (num_groups == 0) { + return data; + } + + const uint32_t len = m_plugin_size(m, STATE_TYPE_GROUPS); + + if (len == 0) { + return data; + } + + data = state_write_section_header(data, STATE_COOKIE_TYPE, len, STATE_TYPE_GROUPS); + + if (!bin_pack_obj(pack_groupchats_handler, c, data, len)) { + LOGGER_FATAL(m->log, "failed to pack group chats into buffer of length %u", len); + return data; + } + + data += len; + + LOGGER_DEBUG(m->log, "Saved %u groups (length %u)", num_groups, len); + + return data; +} + +non_null() +static State_Load_Status groups_load(Messenger *m, const uint8_t *data, uint32_t length) +{ + Bin_Unpack *bu = bin_unpack_new(data, length); + if (bu == nullptr) { + LOGGER_ERROR(m->log, "failed to allocate binary unpacker"); + return STATE_LOAD_STATUS_ERROR; + } + + uint32_t num_groups; + if (!bin_unpack_array(bu, &num_groups)) { + LOGGER_ERROR(m->log, "msgpack failed to unpack groupchats array: expected array"); + bin_unpack_free(bu); + return STATE_LOAD_STATUS_ERROR; + } + + LOGGER_DEBUG(m->log, "Loading %u groups (length %u)", num_groups, length); + + for (uint32_t i = 0; i < num_groups; ++i) { + const int group_number = gc_group_load(m->group_handler, bu); + + if (group_number < 0) { + LOGGER_WARNING(m->log, "Failed to load group %u", i); + } + } + + bin_unpack_free(bu); + + return STATE_LOAD_STATUS_CONTINUE; +} +#endif /* VANILLA_NACL */ + // name state plugin non_null() static uint32_t name_size(const Messenger *m) @@ -3116,6 +3406,9 @@ static void m_register_default_plugins(Messenger *m) m_register_state_plugin(m, STATE_TYPE_STATUSMESSAGE, status_message_size, load_status_message, save_status_message); m_register_state_plugin(m, STATE_TYPE_STATUS, status_size, load_status, save_status); +#ifndef VANILLA_NACL + m_register_state_plugin(m, STATE_TYPE_GROUPS, saved_groups_size, groups_load, groups_save); +#endif m_register_state_plugin(m, STATE_TYPE_TCP_RELAY, tcp_relay_size, load_tcp_relays, save_tcp_relays); m_register_state_plugin(m, STATE_TYPE_PATH_NODE, path_node_size, load_path_nodes, save_path_nodes); } @@ -3288,6 +3581,21 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network return nullptr; } +#ifndef VANILLA_NACL + m->group_announce = new_gca_list(); + + if (m->group_announce == nullptr) { + kill_net_crypto(m->net_crypto); + kill_dht(m->dht); + kill_networking(m->net); + friendreq_kill(m->fr); + logger_kill(m->log); + free(m); + return nullptr; + } + +#endif /* VANILLA_NACL */ + if (options->dht_announcements_enabled) { m->forwarding = new_forwarding(m->log, m->rng, m->mono_time, m->dht); m->announce = new_announcements(m->log, m->rng, m->mono_time, m->forwarding); @@ -3303,10 +3611,35 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network if ((options->dht_announcements_enabled && (m->forwarding == nullptr || m->announce == nullptr)) || m->onion == nullptr || m->onion_a == nullptr || m->onion_c == nullptr || m->fr_c == nullptr) { + kill_onion(m->onion); + kill_onion_announce(m->onion_a); + kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif /* VANILLA_NACL */ kill_friend_connections(m->fr_c); + kill_announcements(m->announce); + kill_forwarding(m->forwarding); + kill_net_crypto(m->net_crypto); + kill_dht(m->dht); + kill_networking(m->net); + friendreq_kill(m->fr); + logger_kill(m->log); + free(m); + return nullptr; + } + +#ifndef VANILLA_NACL + gca_onion_init(m->group_announce, m->onion_a); + + m->group_handler = new_dht_groupchats(m); + + if (m->group_handler == nullptr) { kill_onion(m->onion); kill_onion_announce(m->onion_a); kill_onion_client(m->onion_c); + kill_gca(m->group_announce); + kill_friend_connections(m->fr_c); kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); @@ -3318,15 +3651,23 @@ Messenger *new_messenger(Mono_Time *mono_time, const Random *rng, const Network return nullptr; } +#endif /* VANILLA_NACL */ + if (options->tcp_server_port != 0) { m->tcp_server = new_TCP_server(m->log, m->rng, m->ns, options->ipv6enabled, 1, &options->tcp_server_port, dht_get_self_secret_key(m->dht), m->onion, m->forwarding); if (m->tcp_server == nullptr) { - kill_friend_connections(m->fr_c); kill_onion(m->onion); kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL + kill_dht_groupchats(m->group_handler); +#endif + kill_friend_connections(m->fr_c); kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); @@ -3376,10 +3717,16 @@ void kill_messenger(Messenger *m) kill_TCP_server(m->tcp_server); } - kill_friend_connections(m->fr_c); kill_onion(m->onion); kill_onion_announce(m->onion_a); +#ifndef VANILLA_NACL + kill_dht_groupchats(m->group_handler); +#endif + kill_friend_connections(m->fr_c); kill_onion_client(m->onion_c); +#ifndef VANILLA_NACL + kill_gca(m->group_announce); +#endif kill_announcements(m->announce); kill_forwarding(m->forwarding); kill_net_crypto(m->net_crypto); diff --git a/toxcore/Messenger.h b/toxcore/Messenger.h index 4abd7905d0..8279815277 100644 --- a/toxcore/Messenger.h +++ b/toxcore/Messenger.h @@ -15,6 +15,8 @@ #include "forwarding.h" #include "friend_connection.h" #include "friend_requests.h" +#include "group_announce.h" +#include "group_common.h" #include "logger.h" #include "net_crypto.h" #include "state.h" @@ -35,7 +37,11 @@ typedef enum Message_Type { MESSAGE_ACTION, } Message_Type; +// TODO(Jfreegman, Iphy): Remove this before merge +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED typedef struct Messenger Messenger; +#endif // MESSENGER_DEFINED // Returns the size of the data typedef uint32_t m_state_size_cb(const Messenger *m); @@ -191,6 +197,8 @@ typedef void m_friend_connectionstatuschange_internal_cb(Messenger *m, uint32_t uint8_t connection_status, void *user_data); typedef void m_conference_invite_cb(Messenger *m, uint32_t friend_number, const uint8_t *cookie, uint16_t length, void *user_data); +typedef void m_group_invite_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *userdata); typedef void m_msi_packet_cb(Messenger *m, uint32_t friend_number, const uint8_t *data, uint16_t length, void *user_data); typedef int m_lossy_rtp_packet_cb(Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t len, void *object); @@ -269,6 +277,9 @@ struct Messenger { uint64_t lastdump; uint8_t is_receiving_file; + GC_Session *group_handler; + GC_Announces_List *group_announce; + bool has_added_relays; // If the first connection has occurred in do_messenger uint16_t num_loaded_relays; @@ -288,6 +299,8 @@ struct Messenger { struct Group_Chats *conferences_object; /* Set by new_groupchats()*/ m_conference_invite_cb *conference_invite; + m_group_invite_cb *group_invite; + m_file_recv_cb *file_sendrequest; m_file_recv_control_cb *file_filecontrol; m_file_recv_chunk_cb *file_filedata; @@ -305,6 +318,14 @@ struct Messenger { Messenger_Options options; }; +/** + * Determines if the friendnumber passed is valid in the Messenger object. + * + * @param friendnumber The index in the friend list. + */ +non_null() +bool friend_is_valid(const Messenger *m, int32_t friendnumber); + /** * Format: `[real_pk (32 bytes)][nospam number (4 bytes)][checksum (2 bytes)]` * @@ -348,6 +369,19 @@ int32_t m_addfriend(Messenger *m, const uint8_t *address, const uint8_t *data, u non_null() int32_t m_addfriend_norequest(Messenger *m, const uint8_t *real_pk); +/** @brief Initializes the friend connection and onion connection for a groupchat. + * + * @retval true on success. + */ +non_null() +bool m_create_group_connection(Messenger *m, GC_Chat *chat); + +/* + * Kills the friend connection for a groupchat. + */ +non_null() +void m_kill_group_connection(Messenger *m, const GC_Chat *chat); + /** @return the friend number associated to that public key. * @retval -1 if no such friend. */ @@ -589,6 +623,12 @@ non_null() void m_callback_core_connection(Messenger *m, m_self_connection_statu non_null(1) nullable(2) void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function); +/* Set the callback for group invites. + */ +non_null(1) nullable(2) +void m_callback_group_invite(Messenger *m, m_group_invite_cb *function); + + /** @brief Send a conference invite packet. * * return true on success @@ -597,6 +637,17 @@ void m_callback_conference_invite(Messenger *m, m_conference_invite_cb *function non_null() bool send_conference_invite_packet(const Messenger *m, int32_t friendnumber, const uint8_t *data, uint16_t length); +/* Send a group invite packet. + * + * WARNING: Return-value semantics are different than for + * send_conference_invite_packet(). + * + * return true on success + */ +non_null() +bool send_group_invite_packet(const Messenger *m, uint32_t friendnumber, const uint8_t *data, uint16_t length); + + /*** FILE SENDING */ @@ -678,7 +729,8 @@ int file_seek(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uin * @retval -7 if wrong position. */ non_null(1) nullable(5) -int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, const uint8_t *data, uint16_t length); +int send_file_data(const Messenger *m, int32_t friendnumber, uint32_t filenumber, uint64_t position, + const uint8_t *data, uint16_t length); /*** A/V related */ diff --git a/toxcore/TCP_common.c b/toxcore/TCP_common.c index 349e35fda7..5f3a17919e 100644 --- a/toxcore/TCP_common.c +++ b/toxcore/TCP_common.c @@ -286,7 +286,7 @@ int read_packet_TCP_secure_connection( } if (len_packet != *next_packet_length) { - LOGGER_ERROR(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length); + LOGGER_WARNING(logger, "invalid packet length: %d, expected %d", len_packet, *next_packet_length); return 0; } diff --git a/toxcore/TCP_common.h b/toxcore/TCP_common.h index 88c0cb665a..78d1623d8a 100644 --- a/toxcore/TCP_common.h +++ b/toxcore/TCP_common.h @@ -23,6 +23,20 @@ void wipe_priority_list(TCP_Priority_List *p); #define NUM_RESERVED_PORTS 16 #define NUM_CLIENT_CONNECTIONS (256 - NUM_RESERVED_PORTS) +#ifdef USE_TEST_NETWORK +#define TCP_PACKET_FORWARD_REQUEST 11 +#define TCP_PACKET_FORWARDING 10 +#define TCP_PACKET_ROUTING_REQUEST 9 +#define TCP_PACKET_ROUTING_RESPONSE 8 +#define TCP_PACKET_CONNECTION_NOTIFICATION 7 +#define TCP_PACKET_DISCONNECT_NOTIFICATION 6 +#define TCP_PACKET_PING 5 +#define TCP_PACKET_PONG 4 +#define TCP_PACKET_OOB_SEND 3 +#define TCP_PACKET_OOB_RECV 2 +#define TCP_PACKET_ONION_REQUEST 1 +#define TCP_PACKET_ONION_RESPONSE 0 +#else #define TCP_PACKET_ROUTING_REQUEST 0 #define TCP_PACKET_ROUTING_RESPONSE 1 #define TCP_PACKET_CONNECTION_NOTIFICATION 2 @@ -35,6 +49,7 @@ void wipe_priority_list(TCP_Priority_List *p); #define TCP_PACKET_ONION_RESPONSE 9 #define TCP_PACKET_FORWARD_REQUEST 10 #define TCP_PACKET_FORWARDING 11 +#endif // test network #define TCP_HANDSHAKE_PLAIN_SIZE (CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE) #define TCP_SERVER_HANDSHAKE_SIZE (CRYPTO_NONCE_SIZE + TCP_HANDSHAKE_PLAIN_SIZE + CRYPTO_MAC_SIZE) diff --git a/toxcore/TCP_server.c b/toxcore/TCP_server.c index f571a312d2..6b81322687 100644 --- a/toxcore/TCP_server.c +++ b/toxcore/TCP_server.c @@ -166,7 +166,7 @@ static void free_accepted_connection_array(TCP_Server *tcp_server) tcp_server->size_accepted_connections = 0; } -/** +/** * @return index corresponding to connection with peer on success * @retval -1 on failure. */ @@ -388,7 +388,7 @@ non_null() static int send_routing_response(const Logger *logger, TCP_Secure_Connection *con, uint8_t rpid, const uint8_t *public_key) { - uint8_t data[1 + 1 + CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t data[2 + CRYPTO_PUBLIC_KEY_SIZE]; data[0] = TCP_PACKET_ROUTING_RESPONSE; data[1] = rpid; memcpy(data + 2, public_key, CRYPTO_PUBLIC_KEY_SIZE); diff --git a/toxcore/bin_pack.c b/toxcore/bin_pack.c index ca8940ee44..3575803aed 100644 --- a/toxcore/bin_pack.c +++ b/toxcore/bin_pack.c @@ -127,6 +127,11 @@ bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length) return cmp_write_bin(&bp->ctx, data, length); } +bool bin_pack_nil(Bin_Pack *bp) +{ + return cmp_write_nil(&bp->ctx); +} + bool bin_pack_bin_marker(Bin_Pack *bp, uint32_t size) { return cmp_write_bin_marker(&bp->ctx, size); @@ -159,3 +164,4 @@ bool bin_pack_bin_b(Bin_Pack *bp, const uint8_t *data, uint32_t length) { return bp->ctx.write(&bp->ctx, data, length) == length; } + diff --git a/toxcore/bin_pack.h b/toxcore/bin_pack.h index 542f533b88..51646c088a 100644 --- a/toxcore/bin_pack.h +++ b/toxcore/bin_pack.h @@ -90,9 +90,10 @@ non_null() bool bin_pack_u16(Bin_Pack *bp, uint16_t val); non_null() bool bin_pack_u32(Bin_Pack *bp, uint32_t val); /** @brief Pack a `uint64_t` as MessagePack positive integer. */ non_null() bool bin_pack_u64(Bin_Pack *bp, uint64_t val); +/** @brief Pack an empty array member as a MessagePack nil value. */ +non_null() bool bin_pack_nil(Bin_Pack *bp); /** @brief Pack a byte array as MessagePack bin. */ non_null() bool bin_pack_bin(Bin_Pack *bp, const uint8_t *data, uint32_t length); - /** @brief Start packing a custom binary representation. * * A call to this function must be followed by exactly `size` bytes packed by functions below. diff --git a/toxcore/bin_unpack.c b/toxcore/bin_unpack.c index e4daec3a22..ff591ca87a 100644 --- a/toxcore/bin_unpack.c +++ b/toxcore/bin_unpack.c @@ -104,6 +104,11 @@ bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val) return cmp_read_ulong(&bu->ctx, val); } +bool bin_unpack_nil(Bin_Unpack *bu) +{ + return cmp_read_nil(&bu->ctx); +} + bool bin_unpack_bin(Bin_Unpack *bu, uint8_t **data_ptr, uint32_t *data_length_ptr) { uint32_t bin_size; diff --git a/toxcore/bin_unpack.h b/toxcore/bin_unpack.h index c6ead96e1b..bd4d8785c1 100644 --- a/toxcore/bin_unpack.h +++ b/toxcore/bin_unpack.h @@ -60,6 +60,9 @@ non_null() bool bin_unpack_u16(Bin_Unpack *bu, uint16_t *val); non_null() bool bin_unpack_u32(Bin_Unpack *bu, uint32_t *val); /** @brief Unpack a MessagePack positive int into a `uint64_t`. */ non_null() bool bin_unpack_u64(Bin_Unpack *bu, uint64_t *val); +/** @brief Unpack a Messagepack nil value. */ +non_null() bool bin_unpack_nil(Bin_Unpack *bu); + /** @brief Unpack a MessagePack bin into a newly allocated byte array. * * Allocates a new byte array and stores it into `data_ptr` with its length stored in diff --git a/toxcore/friend_connection.c b/toxcore/friend_connection.c index c65e3105ba..7b8537c720 100644 --- a/toxcore/friend_connection.c +++ b/toxcore/friend_connection.c @@ -197,7 +197,7 @@ Friend_Conn *get_conn(const Friend_Connections *fr_c, int friendcon_id) return &fr_c->conns[friendcon_id]; } -/** +/** * @return friendcon_id corresponding to the real public key on success. * @retval -1 on failure. */ @@ -882,7 +882,8 @@ int send_friend_request_packet(Friend_Connections *fr_c, int friendcon_id, uint3 if (friend_con->status == FRIENDCONN_STATUS_CONNECTED) { packet[0] = PACKET_ID_FRIEND_REQUESTS; - return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), false) != -1 ? 1 : 0; + return write_cryptpacket(fr_c->net_crypto, friend_con->crypt_connection_id, packet, SIZEOF_VLA(packet), + false) != -1 ? 1 : 0; } packet[0] = CRYPTO_PACKET_FRIEND_REQ; diff --git a/toxcore/group_announce.c b/toxcore/group_announce.c index 83dd3acf54..896b043dbd 100644 --- a/toxcore/group_announce.c +++ b/toxcore/group_announce.c @@ -186,7 +186,7 @@ static int gca_unpack_announce(const Logger *log, const uint8_t *data, uint16_t memcpy(announce->peer_public_key, data + offset, ENC_PUBLIC_KEY_SIZE); offset += ENC_PUBLIC_KEY_SIZE; - announce->ip_port_is_set = data[offset] == 1; + net_unpack_bool(&data[offset], &announce->ip_port_is_set); ++offset; announce->tcp_relays_count = data[offset]; diff --git a/toxcore/group_announce.h b/toxcore/group_announce.h index 11bba7d530..801363d6b2 100644 --- a/toxcore/group_announce.h +++ b/toxcore/group_announce.h @@ -20,7 +20,7 @@ extern "C" { /* The maximum number of announces to save for a particular group chat. */ #define GCA_MAX_SAVED_ANNOUNCES_PER_GC 16 -/* Maximum number of TCP relays that can be in an annoucne. */ +/* Maximum number of TCP relays that can be in an announce. */ #define GCA_MAX_ANNOUNCED_TCP_RELAYS 1 /* Maximum number of announces we can send in an announce response. */ diff --git a/toxcore/group_chats.c b/toxcore/group_chats.c new file mode 100644 index 0000000000..318c6aba06 --- /dev/null +++ b/toxcore/group_chats.c @@ -0,0 +1,8373 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_chats.h" + +#include + +#ifndef VANILLA_NACL +#include +#endif + +#include + +#include "DHT.h" +#include "LAN_discovery.h" +#include "Messenger.h" +#include "TCP_connection.h" +#include "ccompat.h" +#include "friend_connection.h" +#include "group_common.h" +#include "group_moderation.h" +#include "group_pack.h" +#include "mono_time.h" +#include "network.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/* The minimum size of a plaintext group handshake packet */ +#define GC_MIN_HS_PACKET_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) + +/* The minimum size of an encrypted group handshake packet. */ +#define GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE (1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE +\ + GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) + +/* Size of a group's shared state in packed format */ +#define GC_PACKED_SHARED_STATE_SIZE (EXT_PUBLIC_KEY_SIZE + sizeof(uint16_t) + MAX_GC_GROUP_NAME_SIZE +\ + sizeof(uint16_t) + 1 + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE +\ + MOD_MODERATION_HASH_SIZE + sizeof(uint32_t) + sizeof(uint32_t) + 1) + +/* Minimum size of a topic packet; includes topic length, public signature key, topic version and checksum */ +#define GC_MIN_PACKED_TOPIC_INFO_SIZE (sizeof(uint16_t) + SIG_PUBLIC_KEY_SIZE + sizeof(uint32_t) + sizeof(uint16_t)) + +#define GC_SHARED_STATE_ENC_PACKET_SIZE (SIGNATURE_SIZE + GC_PACKED_SHARED_STATE_SIZE) + +/* Header information attached to all broadcast messages: broadcast_type */ +#define GC_BROADCAST_ENC_HEADER_SIZE 1 + +/* Size of a group packet message ID */ +#define GC_MESSAGE_ID_BYTES sizeof(uint64_t) + +/* Size of a lossless ack packet */ +#define GC_LOSSLESS_ACK_PACKET_SIZE (GC_MESSAGE_ID_BYTES + 1) + +/* Smallest possible size of an encrypted lossless payload. + * + * Data includes the message_id, group packet type, and the nonce and MAC for decryption. + */ +#define GC_MIN_LOSSLESS_PAYLOAD_SIZE (GC_MESSAGE_ID_BYTES + CRYPTO_NONCE_SIZE + 1 + CRYPTO_MAC_SIZE) + +/* Smallest possible size of a lossy group packet */ +#define GC_MIN_LOSSY_PAYLOAD_SIZE (GC_MIN_LOSSLESS_PAYLOAD_SIZE - GC_MESSAGE_ID_BYTES) + +/* Maximum number of bytes to pad packets with. + * + * Packets are padded with a random number of zero bytes between zero and this value in order to hide + * the true length of the message, which reduces the amount of metadata leaked through packet analysis. + * + * Note: This behaviour was copied from the toxcore encryption implementation in net_crypto.c. + */ +#define GC_MAX_PACKET_PADDING 8 + +/* Minimum size of a ping packet, which contains the peer count, peer list checksum, shared state version, + * sanctions list version, sanctions list checksum, topic version, and topic checksum + */ +#define GC_PING_PACKET_MIN_DATA_SIZE ((sizeof(uint16_t) * 4) + (sizeof(uint32_t) * 3)) + +/* How often in seconds we can send a group sync request packet */ +#define GC_SYNC_REQUEST_LIMIT (GC_PING_TIMEOUT + 1) + +/* How often in seconds we can send the peer list to any peer in the group in a sync response */ +#define GC_SYNC_RESPONSE_PEER_LIST_LIMIT 3 + +/* How often in seconds we try to handshake with an unconfirmed peer */ +#define GC_SEND_HANDSHAKE_INTERVAL 3 + +/* How often in seconds we rotate session encryption keys with a peer */ +#define GC_KEY_ROTATION_TIMEOUT (5 * 60) + +/* How often in seconds we try to reconnect to peers that recently timed out */ +#define GC_TIMED_OUT_RECONN_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT * 3) + +/* How long in seconds before we stop trying to reconnect with a timed out peer */ +#define GC_TIMED_OUT_STALE_TIMEOUT (60 * 15) + +/* The value the topic lock is set to when the topic lock is enabled. */ +#define GC_TOPIC_LOCK_ENABLED 0 + +static_assert(GCC_BUFFER_SIZE <= UINT16_MAX, + "GCC_BUFFER_SIZE must be <= UINT16_MAX)"); + +static_assert(MAX_GC_PACKET_CHUNK_SIZE < MAX_GC_PACKET_SIZE, + "MAX_GC_PACKET_CHUNK_SIZE must be < MAX_GC_PACKET_SIZE"); + +// size of a lossless handshake packet - lossless packets can't/shouldn't be split up +static_assert(MAX_GC_PACKET_CHUNK_SIZE >= 171, + "MAX_GC_PACKET_CHUNK_SIZE must be >= 171"); + +// group_moderation constants assume this is the max packet size. +static_assert(MAX_GC_PACKET_SIZE >= 50000, + "MAX_GC_PACKET_SIZE doesn't match constants in group_moderation.h"); + +static_assert(MAX_GC_PACKET_SIZE <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE, + "MAX_GC_PACKET_SIZE must be <= UINT16_MAX - MAX_GC_PACKET_CHUNK_SIZE"); + +/** Types of broadcast messages. */ +typedef enum Group_Message_Type { + GC_MESSAGE_TYPE_NORMAL = 0x00, + GC_MESSAGE_TYPE_ACTION = 0x01, +} Group_Message_Type; + +/** Types of handshake request packets. */ +typedef enum Group_Handshake_Packet_Type { + GH_REQUEST = 0x00, // Requests a handshake + GH_RESPONSE = 0x01, // Responds to a handshake request +} Group_Handshake_Packet_Type; + +/** Types of handshake requests (within a handshake request packet). */ +typedef enum Group_Handshake_Request_Type { + HS_INVITE_REQUEST = 0x00, // Requests an invite to the group + HS_PEER_INFO_EXCHANGE = 0x01, // Requests a peer info exchange +} Group_Handshake_Request_Type; + +/** These bitmasks determine what group state info a peer is requesting in a sync request */ +typedef enum Group_Sync_Flags { + GF_PEERS = (1 << 0), // 1 + GF_TOPIC = (1 << 1), // 2 + GF_STATE = (1 << 2), // 4 +} Group_Sync_Flags; + +non_null() static bool self_gc_is_founder(const GC_Chat *chat); +non_null() static bool group_number_valid(const GC_Session *c, int group_number); +non_null() static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number); +non_null() static void group_delete(GC_Session *c, GC_Chat *chat); +non_null() static void group_cleanup(GC_Session *c, GC_Chat *chat); +non_null() static bool group_exists(const GC_Session *c, const uint8_t *chat_id); +non_null() static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat); +non_null(1, 2) nullable(4) +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata); +non_null() static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, + uint8_t *secret_key); +non_null() static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs); +non_null() static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer); + +static const GC_Chat empty_gc_chat = {nullptr}; + +non_null() +static void kill_group_friend_connection(const GC_Session *c, const GC_Chat *chat) +{ + if (chat->friend_connection_id != -1) { + m_kill_group_connection(c->messenger, chat); + } +} + +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type) +{ + assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + + const uint16_t min_header_size = packet_type == NET_PACKET_GC_LOSSY + ? GC_MIN_LOSSY_PAYLOAD_SIZE + : GC_MIN_LOSSLESS_PAYLOAD_SIZE; + const uint16_t header_size = ENC_PUBLIC_KEY_SIZE + GC_MAX_PACKET_PADDING + min_header_size; + + assert(length <= UINT16_MAX - header_size); + + return length + header_size; +} + +/** Return true if `peer_number` is our own. */ +static bool peer_number_is_self(int peer_number) +{ + return peer_number == 0; +} + +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number) +{ + return peer_number >= 0 && peer_number < (int)chat->numpeers; +} + +non_null() +static GC_Peer *get_gc_peer(const GC_Chat *chat, int peer_number) +{ + if (!gc_peer_number_is_valid(chat, peer_number)) { + return nullptr; + } + + return &chat->group[peer_number]; +} + +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return nullptr; + } + + return &peer->gconn; +} + +/** Returns the amount of empty padding a packet of designated length should have. */ +static uint16_t group_packet_padding_length(uint16_t length) +{ + return (MAX_GC_PACKET_CHUNK_SIZE - length) % GC_MAX_PACKET_PADDING; +} + +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick) +{ + if (nick != nullptr) { + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + assert(peer->nick_length > 0); + + memcpy(nick, peer->nick, peer->nick_length); + } +} + +uint16_t gc_get_self_nick_size(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->nick_length; +} + +/** @brief Sets self nick to `nick`. + * + * Returns false if `nick` is null or `length` is greater than MAX_GC_NICK_SIZE. + */ +non_null() +static bool self_gc_set_nick(const GC_Chat *chat, const uint8_t *nick, uint16_t length) +{ + if (nick == nullptr || length > MAX_GC_NICK_SIZE) { + return false; + } + + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + memcpy(peer->nick, nick, length); + peer->nick_length = length; + + return true; +} + +Group_Role gc_get_self_role(const GC_Chat *chat) +{ + + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->role; +} + +/** Sets self role. If role is invalid this function has no effect. */ +non_null() +static void self_gc_set_role(const GC_Chat *chat, Group_Role role) +{ + if (role <= GR_OBSERVER) { + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + peer->role = role; + } +} + +uint8_t gc_get_self_status(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->status; +} + +/** Sets self status. If status is invalid this function has no effect. */ +non_null() +static void self_gc_set_status(const GC_Chat *chat, Group_Peer_Status status) +{ + if (status == GS_NONE || status == GS_AWAY || status == GS_BUSY) { + GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + peer->status = status; + return; + } + + LOGGER_WARNING(chat->log, "Attempting to set user status with invalid status: %u", (uint8_t)status); +} + +uint32_t gc_get_self_peer_id(const GC_Chat *chat) +{ + const GC_Peer *peer = get_gc_peer(chat, 0); + assert(peer != nullptr); + + return peer->peer_id; +} + +/** Sets self confirmed status. */ +non_null() +static void self_gc_set_confirmed(const GC_Chat *chat, bool confirmed) +{ + GC_Connection *gconn = get_gc_connection(chat, 0); + assert(gconn != nullptr); + + gconn->confirmed = confirmed; +} + +/** Returns true if self has the founder role */ +non_null() +static bool self_gc_is_founder(const GC_Chat *chat) +{ + return gc_get_self_role(chat) == GR_FOUNDER; +} + +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key) +{ + if (public_key != nullptr) { + memcpy(public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + } +} + +/** @brief Sets self extended public key to `ext_public_key`. + * + * If `ext_public_key` is null this function has no effect. + */ +non_null() +static void self_gc_set_ext_public_key(const GC_Chat *chat, const uint8_t *ext_public_key) +{ + if (ext_public_key != nullptr) { + GC_Connection *gconn = get_gc_connection(chat, 0); + assert(gconn != nullptr); + memcpy(gconn->addr.public_key, ext_public_key, EXT_PUBLIC_KEY_SIZE); + } +} + +/** + * Return true if `peer` has permission to speak according to the `voice_state`. + */ +non_null() +static bool peer_has_voice(const GC_Peer *peer, Group_Voice_State voice_state) +{ + const Group_Role role = peer->role; + + switch (voice_state) { + case GV_ALL: + return role <= GR_USER; + + case GV_MODS: + return role <= GR_MODERATOR; + + case GV_FOUNDER: + return role == GR_FOUNDER; + + default: + return false; + } +} + +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed) +{ + uint16_t packed_len = 0; + uint16_t count = 0; + + for (uint32_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (!saved_peer_is_valid(saved_peer)) { + continue; + } + + int packed_ipp_len = 0; + int packed_tcp_len = 0; + + if (ipport_isset(&saved_peer->ip_port)) { + if (packed_len > length) { + return -1; + } + + packed_ipp_len = pack_ip_port(chat->log, data + packed_len, length - packed_len, &saved_peer->ip_port); + + if (packed_ipp_len > 0) { + packed_len += packed_ipp_len; + } + } + + if (ipport_isset(&saved_peer->tcp_relay.ip_port)) { + if (packed_len > length) { + return -1; + } + + packed_tcp_len = pack_nodes(chat->log, data + packed_len, length - packed_len, &saved_peer->tcp_relay, 1); + + if (packed_tcp_len > 0) { + packed_len += packed_tcp_len; + } + } + + if (packed_len + ENC_PUBLIC_KEY_SIZE > length) { + return -1; + } + + if (packed_tcp_len > 0 || packed_ipp_len > 0) { + memcpy(data + packed_len, chat->saved_peers[i].public_key, ENC_PUBLIC_KEY_SIZE); + packed_len += ENC_PUBLIC_KEY_SIZE; + ++count; + } else { + LOGGER_WARNING(chat->log, "Failed to pack saved peer"); + } + } + + if (processed != nullptr) { + *processed = packed_len; + } + + return count; +} + +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length) +{ + uint16_t count = 0; + uint16_t unpacked_len = 0; + + for (size_t i = 0; unpacked_len < length; ++i) { + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + const int ipp_len = unpack_ip_port(&saved_peer->ip_port, data + unpacked_len, length - unpacked_len, false); + + if (ipp_len > 0) { + unpacked_len += ipp_len; + } + + if (unpacked_len > length) { + return -1; + } + + uint16_t tcp_len_processed = 0; + const int tcp_len = unpack_nodes(&saved_peer->tcp_relay, 1, &tcp_len_processed, data + unpacked_len, + length - unpacked_len, true); + + if (tcp_len == 1 && tcp_len_processed > 0) { + unpacked_len += tcp_len_processed; + } else if (ipp_len <= 0) { + LOGGER_WARNING(chat->log, "Failed to unpack saved peer: Invalid connection info."); + return -1; + } + + if (unpacked_len + ENC_PUBLIC_KEY_SIZE > length) { + return -1; + } + + if (tcp_len > 0 || ipp_len > 0) { + memcpy(saved_peer->public_key, data + unpacked_len, ENC_PUBLIC_KEY_SIZE); + unpacked_len += ENC_PUBLIC_KEY_SIZE; + ++count; + } else { + LOGGER_ERROR(chat->log, "Unpacked peer with bad connection info"); + return -1; + } + } + + return count; +} + +/** Returns true if chat privacy state is set to public. */ +non_null() +static bool is_public_chat(const GC_Chat *chat) +{ + return chat->shared_state.privacy_state == GI_PUBLIC; +} + +/** Returns true if group is password protected */ +non_null() +static bool chat_is_password_protected(const GC_Chat *chat) +{ + return chat->shared_state.password_length > 0; +} + +/** Returns true if `password` matches the current group password. */ +non_null() +static bool validate_password(const GC_Chat *chat, const uint8_t *password, uint16_t length) +{ + if (length > MAX_GC_PASSWORD_SIZE) { + return false; + } + + if (length != chat->shared_state.password_length) { + return false; + } + + return memcmp(chat->shared_state.password, password, length) == 0; +} + +/** @brief Returns the chat object that contains a peer with a public key equal to `id`. + * + * `id` must be at least ENC_PUBLIC_KEY_SIZE bytes in length. + */ +non_null() +static GC_Chat *get_chat_by_id(const GC_Session *c, const uint8_t *id) +{ + if (c == nullptr) { + return nullptr; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(id, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return chat; + } + + if (get_peer_number_of_enc_pk(chat, id, false) != -1) { + return chat; + } + } + + return nullptr; +} + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key) +{ + return jenkins_one_at_a_time_hash(public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** @brief Sets the sum of the public_key_hash of all confirmed peers. + * + * Must be called every time a peer is confirmed or deleted. + */ +non_null() +static void set_gc_peerlist_checksum(GC_Chat *chat) +{ + uint16_t sum = 0; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + sum += gconn->public_key_hash; + } + } + + chat->peers_checksum = sum; +} + +/** Returns a checksum of the topic currently set in `topic_info`. */ +non_null() +static uint16_t get_gc_topic_checksum(const GC_TopicInfo *topic_info) +{ + return data_checksum(topic_info->topic, topic_info->length); +} + +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->pending_delete) { + continue; + } + + if (confirmed && !gconn->confirmed) { + continue; + } + + if (memcmp(gconn->addr.public_key, public_enc_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +/** @brief Check if peer associated with `public_sig_key` is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + */ +non_null() +static int get_peer_number_of_sig_pk(const GC_Chat *chat, const uint8_t *public_sig_key) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (memcmp(get_sig_pk(gconn->addr.public_key), public_sig_key, SIG_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +non_null() +static bool gc_get_enc_pk_from_sig_pk(const GC_Chat *chat, uint8_t *public_key, const uint8_t *public_sig_key) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + const uint8_t *full_pk = gconn->addr.public_key; + + if (memcmp(public_sig_key, get_sig_pk(full_pk), SIG_PUBLIC_KEY_SIZE) == 0) { + memcpy(public_key, get_enc_key(full_pk), ENC_PUBLIC_KEY_SIZE); + return true; + } + } + + return false; +} + +non_null() +static GC_Connection *random_gc_connection(const GC_Chat *chat) +{ + if (chat->numpeers <= 1) { + return nullptr; + } + + const uint32_t base = random_range_u32(chat->rng, chat->numpeers - 1); + + for (uint32_t i = 0; i < chat->numpeers - 1; ++i) { + const uint32_t index = 1 + (base + i) % (chat->numpeers - 1); + GC_Connection *rand_gconn = get_gc_connection(chat, index); + + if (rand_gconn == nullptr) { + return nullptr; + } + + if (!rand_gconn->pending_delete && rand_gconn->confirmed) { + return rand_gconn; + } + } + + return nullptr; +} + +/** @brief Returns the peer number associated with peer_id. + * Returns -1 if peer_id is invalid. + */ +non_null() +static int get_peer_number_of_peer_id(const GC_Chat *chat, uint32_t peer_id) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + if (chat->group[i].peer_id == peer_id) { + return i; + } + } + + return -1; +} + +/** @brief Returns a unique peer ID. + * Returns UINT32_MAX if all possible peer ID's are taken. + * + * These ID's are permanently assigned to a peer when they join the group and should be + * considered arbitrary values. + */ +non_null() +static uint32_t get_new_peer_id(const GC_Chat *chat) +{ + for (uint32_t i = 0; i < UINT32_MAX - 1; ++i) { + if (get_peer_number_of_peer_id(chat, i) == -1) { + return i; + } + } + + return UINT32_MAX; +} + +/** @brief Sets the password for the group (locally only). + * + * Return true on success. + */ +non_null(1) nullable(2) +static bool set_gc_password_local(GC_Chat *chat, const uint8_t *passwd, uint16_t length) +{ + if (length > MAX_GC_PASSWORD_SIZE) { + return false; + } + + if (passwd == nullptr || length == 0) { + chat->shared_state.password_length = 0; + memset(chat->shared_state.password, 0, MAX_GC_PASSWORD_SIZE); + } else { + chat->shared_state.password_length = length; + crypto_memlock(chat->shared_state.password, sizeof(chat->shared_state.password)); + memcpy(chat->shared_state.password, passwd, length); + } + + return true; +} + +/** @brief Sets the local shared state to `version`. + * + * This should always be called instead of setting the variables manually. + */ +non_null() +static void set_gc_shared_state_version(GC_Chat *chat, uint32_t version) +{ + chat->shared_state.version = version; + chat->moderation.shared_state_version = version; +} + +/** @brief Expands the chat_id into the extended chat public key (encryption key + signature key). + * + * @param dest must have room for EXT_PUBLIC_KEY_SIZE bytes. + * + * Return true on success. + */ +non_null() +static bool expand_chat_id(uint8_t *dest, const uint8_t *chat_id) +{ + assert(dest != nullptr); + + const int ret = crypto_sign_ed25519_pk_to_curve25519(dest, chat_id); + memcpy(dest + ENC_PUBLIC_KEY_SIZE, chat_id, SIG_PUBLIC_KEY_SIZE); + + return ret != -1; +} + +/** Copies peer connect info from `gconn` to `addr`. */ +non_null() +static void copy_gc_saved_peer(const Random *rng, const GC_Connection *gconn, GC_SavedPeerInfo *addr) +{ + if (!gcc_copy_tcp_relay(rng, &addr->tcp_relay, gconn)) { + addr->tcp_relay = (Node_format) { + 0 + }; + } + + addr->ip_port = gconn->addr.ip_port; + memcpy(addr->public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); +} + +/** Return true if `saved_peer` has either a valid IP_Port or a valid TCP relay. */ +non_null() +static bool saved_peer_is_valid(const GC_SavedPeerInfo *saved_peer) +{ + return ipport_isset(&saved_peer->ip_port) || ipport_isset(&saved_peer->tcp_relay.ip_port); +} + +/** @brief Returns the index of the saved peers entry for `public_key`. + * Returns -1 if key is not found. + */ +non_null() +static int saved_peer_index(const GC_Chat *chat, const uint8_t *public_key) +{ + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (memcmp(saved_peer->public_key, public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + return i; + } + } + + return -1; +} + +/** @brief Returns the index of the first vacant entry in saved peers list. + * + * If `public_key` is non-null and already exists in the list, its index will be returned. + * + * A vacant entry is an entry that does not have either an IP_port or tcp relay set (invalid), + * or an entry containing info on a peer that is not presently online (offline). + * + * Invalid entries are given priority over offline entries. + * + * Returns -1 if there are no vacant indices. + */ +non_null(1) nullable(2) +static int saved_peers_get_new_index(const GC_Chat *chat, const uint8_t *public_key) +{ + if (public_key != nullptr) { + const int idx = saved_peer_index(chat, public_key); + + if (idx != -1) { + return idx; + } + } + + // first check for invalid spots + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + if (!saved_peer_is_valid(saved_peer)) { + return i; + } + } + + // now look for entries with offline peers + for (uint16_t i = 0; i < GC_MAX_SAVED_PEERS; ++i) { + const GC_SavedPeerInfo *saved_peer = &chat->saved_peers[i]; + + const int peernumber = get_peer_number_of_enc_pk(chat, saved_peer->public_key, true); + + if (peernumber < 0) { + return i; + } + } + + return -1; +} + +/** @brief Attempts to add `gconn` to the saved peer list. + * + * If an entry already exists it will be updated. + * + * Older peers will only be overwritten if the peer is no longer + * present in the chat. This gives priority to more stable connections. + * + * This function should be called every time a new peer joins the group. + */ +non_null() +static void add_gc_saved_peers(GC_Chat *chat, const GC_Connection *gconn) +{ + const int idx = saved_peers_get_new_index(chat, gconn->addr.public_key); + + if (idx == -1) { + return; + } + + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; + copy_gc_saved_peer(chat->rng, gconn, saved_peer); +} + +/** @brief Finds the first vacant spot in the saved peers list and fills it with a present + * peer who isn't already in the list. + * + * This function should be called after a confirmed peer exits the group. + */ +non_null() +static void refresh_gc_saved_peers(GC_Chat *chat) +{ + const int idx = saved_peers_get_new_index(chat, nullptr); + + if (idx == -1) { + return; + } + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + if (gconn == nullptr) { + continue; + } + + if (!gconn->confirmed) { + continue; + } + + if (saved_peer_index(chat, gconn->addr.public_key) == -1) { + GC_SavedPeerInfo *saved_peer = &chat->saved_peers[idx]; + copy_gc_saved_peer(chat->rng, gconn, saved_peer); + return; + } + } +} + +/** Returns the number of confirmed peers in peerlist. */ +non_null() +static uint16_t get_gc_confirmed_numpeers(const GC_Chat *chat) +{ + uint16_t count = 0; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + ++count; + } + } + + return count; +} + +non_null() static bool sign_gc_shared_state(GC_Chat *chat); +non_null() static bool broadcast_gc_mod_list(const GC_Chat *chat); +non_null() static bool broadcast_gc_shared_state(const GC_Chat *chat); +non_null() static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key); +non_null() static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, + const uint8_t *sanction_data, uint16_t length, bool add_obs); + +/** Returns true if peer designated by `peer_number` is in the sanctions list as an observer. */ +non_null() +static bool peer_is_observer(const GC_Chat *chat, uint32_t peer_number) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + return sanctions_list_is_observer(&chat->moderation, get_enc_key(gconn->addr.public_key)); +} + +/** Returns true if peer designated by `peer_number` is the group founder. */ +non_null() +static bool peer_is_founder(const GC_Chat *chat, uint32_t peer_number) +{ + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + return memcmp(chat->shared_state.founder_public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE) == 0; +} + +/** Returns true if peer designated by `peer_number` is in the moderator list or is the founder. */ +non_null() +static bool peer_is_moderator(const GC_Chat *chat, uint32_t peer_number) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (peer_is_founder(chat, peer_number)) { + return false; + } + + return mod_list_verify_sig_pk(&chat->moderation, get_sig_pk(gconn->addr.public_key)); +} + +/** @brief Iterates through the peerlist and updates group roles according to the + * current group state. + * + * Also updates the roles checksum. If any role conflicts exist the shared state + * version is set to zero in order to force a sync update. + * + * This should be called every time the moderator list or sanctions list changes, + * and after a new peer is marked as confirmed. + */ +non_null() +static void update_gc_peer_roles(GC_Chat *chat) +{ + chat->roles_checksum = 0; + bool conflicts = false; + + for (uint32_t i = 0; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + if (gconn == nullptr) { + continue; + } + + if (!gconn->confirmed) { + continue; + } + + const uint8_t first_byte = gconn->addr.public_key[0]; + const bool is_founder = peer_is_founder(chat, i); + + if (is_founder) { + chat->group[i].role = GR_FOUNDER; + chat->roles_checksum += GR_FOUNDER + first_byte; + continue; + } + + const bool is_observer = peer_is_observer(chat, i); + const bool is_moderator = peer_is_moderator(chat, i); + const bool is_user = !(is_founder || is_moderator || is_observer); + + if (is_observer && is_moderator) { + conflicts = true; + } + + if (is_user) { + chat->group[i].role = GR_USER; + chat->roles_checksum += GR_USER + first_byte; + continue; + } + + if (is_moderator) { + chat->group[i].role = GR_MODERATOR; + chat->roles_checksum += GR_MODERATOR + first_byte; + continue; + } + + if (is_observer) { + chat->group[i].role = GR_OBSERVER; + chat->roles_checksum += GR_OBSERVER + first_byte; + continue; + } + } + + if (conflicts && !self_gc_is_founder(chat)) { + set_gc_shared_state_version(chat, 0); // need a new shared state + } +} + +/** @brief Removes the first found offline mod from the mod list. + * + * Broadcasts the shared state and moderator list on success, as well as the updated + * sanctions list if necessary. + * + * TODO(Jfreegman): Make this smarter in who to remove (e.g. the mod who hasn't been seen online in the longest time) + * + * Returns false on failure. + */ +non_null() +static bool prune_gc_mod_list(GC_Chat *chat) +{ + if (chat->moderation.num_mods == 0) { + return true; + } + + uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; + bool pruned_mod = false; + + for (uint16_t i = 0; i < chat->moderation.num_mods; ++i) { + if (get_peer_number_of_sig_pk(chat, chat->moderation.mod_list[i]) == -1) { + memcpy(public_sig_key, chat->moderation.mod_list[i], SIG_PUBLIC_KEY_SIZE); + + if (!mod_list_remove_index(&chat->moderation, i)) { + continue; + } + + pruned_mod = true; + break; + } + } + + return pruned_mod + && mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash) + && sign_gc_shared_state(chat) + && broadcast_gc_shared_state(chat) + && broadcast_gc_mod_list(chat) + && update_gc_sanctions_list(chat, public_sig_key) + && update_gc_topic(chat, public_sig_key); +} + +/** @brief Removes the first found offline sanctioned peer from the sanctions list and sends the + * event to the rest of the group. + * + * @retval false on failure or if no presently sanctioned peer is offline. + */ +non_null() +static bool prune_gc_sanctions_list(GC_Chat *chat) +{ + if (chat->moderation.num_sanctions == 0) { + return true; + } + + const Mod_Sanction *sanction = nullptr; + uint8_t target_ext_pk[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + + for (uint16_t i = 0; i < chat->moderation.num_sanctions; ++i) { + const int peer_number = get_peer_number_of_enc_pk(chat, chat->moderation.sanctions[i].target_public_enc_key, true); + + if (peer_number == -1) { + sanction = &chat->moderation.sanctions[i]; + memcpy(target_ext_pk, sanction->target_public_enc_key, ENC_PUBLIC_KEY_SIZE); + memcpy(target_ext_pk + ENC_PUBLIC_KEY_SIZE, sanction->setter_public_sig_key, SIG_PUBLIC_KEY_SIZE); + break; + } + } + + if (sanction == nullptr) { + return false; + } + + if (!sanctions_list_remove_observer(&chat->moderation, sanction->target_public_enc_key, nullptr)) { + LOGGER_WARNING(chat->log, "Failed to remove entry from observer list"); + return false; + } + + sanction = nullptr; + + uint8_t data[MOD_SANCTIONS_CREDS_SIZE]; + const uint16_t length = sanctions_creds_pack(&chat->moderation.sanctions_creds, data); + + if (length != MOD_SANCTIONS_CREDS_SIZE) { + LOGGER_ERROR(chat->log, "Failed to pack credentials (invalid length: %u)", length); + return false; + } + + if (!send_gc_set_observer(chat, target_ext_pk, data, length, false)) { + LOGGER_WARNING(chat->log, "Failed to broadcast set observer"); + return false; + } + + return true; +} + +/** @brief Size of peer data that we pack for transfer (nick length must be accounted for separately). + * packed data consists of: nick length, nick, and status. + */ +#define PACKED_GC_PEER_SIZE (sizeof(uint16_t) + MAX_GC_NICK_SIZE + sizeof(uint8_t)) + +/** @brief Packs peer info into data of maxlength length. + * + * Return length of packed peer on success. + * Return -1 on failure. + */ +non_null() +static int pack_gc_peer(uint8_t *data, uint16_t length, const GC_Peer *peer) +{ + if (PACKED_GC_PEER_SIZE > length) { + return -1; + } + + uint32_t packed_len = 0; + + net_pack_u16(data + packed_len, peer->nick_length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, peer->nick, MAX_GC_NICK_SIZE); + packed_len += MAX_GC_NICK_SIZE; + memcpy(data + packed_len, &peer->status, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + + return packed_len; +} + +/** @brief Unpacks peer info of size length into peer. + * + * Returns the length of processed data on success. + * Returns -1 on failure. + */ +non_null() +static int unpack_gc_peer(GC_Peer *peer, const uint8_t *data, uint16_t length) +{ + if (PACKED_GC_PEER_SIZE > length) { + return -1; + } + + uint16_t len_processed = 0; + + net_unpack_u16(data + len_processed, &peer->nick_length); + len_processed += sizeof(uint16_t); + peer->nick_length = min_u16(MAX_GC_NICK_SIZE, peer->nick_length); + memcpy(peer->nick, data + len_processed, MAX_GC_NICK_SIZE); + len_processed += MAX_GC_NICK_SIZE; + memcpy(&peer->status, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + return len_processed; +} + +/** @brief Packs shared_state into data. + * + * @param data must have room for at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_shared_state(uint8_t *data, uint16_t length, const GC_SharedState *shared_state) +{ + if (length < GC_PACKED_SHARED_STATE_SIZE) { + return 0; + } + + const uint8_t privacy_state = shared_state->privacy_state; + const uint8_t voice_state = shared_state->voice_state; + + uint16_t packed_len = 0; + + // version is always first + net_pack_u32(data + packed_len, shared_state->version); + packed_len += sizeof(uint32_t); + + memcpy(data + packed_len, shared_state->founder_public_key, EXT_PUBLIC_KEY_SIZE); + packed_len += EXT_PUBLIC_KEY_SIZE; + net_pack_u16(data + packed_len, shared_state->maxpeers); + packed_len += sizeof(uint16_t); + net_pack_u16(data + packed_len, shared_state->group_name_len); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, shared_state->group_name, MAX_GC_GROUP_NAME_SIZE); + packed_len += MAX_GC_GROUP_NAME_SIZE; + memcpy(data + packed_len, &privacy_state, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + net_pack_u16(data + packed_len, shared_state->password_length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, shared_state->password, MAX_GC_PASSWORD_SIZE); + packed_len += MAX_GC_PASSWORD_SIZE; + memcpy(data + packed_len, shared_state->mod_list_hash, MOD_MODERATION_HASH_SIZE); + packed_len += MOD_MODERATION_HASH_SIZE; + net_pack_u32(data + packed_len, shared_state->topic_lock); + packed_len += sizeof(uint32_t); + memcpy(data + packed_len, &voice_state, sizeof(uint8_t)); + packed_len += sizeof(uint8_t); + + return packed_len; +} + +/** @brief Unpacks shared state data into shared_state. + * + * @param data must contain at least GC_PACKED_SHARED_STATE_SIZE bytes. + * + * Returns the length of processed data. + */ +non_null() +static uint16_t unpack_gc_shared_state(GC_SharedState *shared_state, const uint8_t *data, uint16_t length) +{ + if (length < GC_PACKED_SHARED_STATE_SIZE) { + return 0; + } + + uint16_t len_processed = 0; + + // version is always first + net_unpack_u32(data + len_processed, &shared_state->version); + len_processed += sizeof(uint32_t); + + memcpy(shared_state->founder_public_key, data + len_processed, EXT_PUBLIC_KEY_SIZE); + len_processed += EXT_PUBLIC_KEY_SIZE; + net_unpack_u16(data + len_processed, &shared_state->maxpeers); + len_processed += sizeof(uint16_t); + net_unpack_u16(data + len_processed, &shared_state->group_name_len); + shared_state->group_name_len = min_u16(shared_state->group_name_len, MAX_GC_GROUP_NAME_SIZE); + len_processed += sizeof(uint16_t); + memcpy(shared_state->group_name, data + len_processed, MAX_GC_GROUP_NAME_SIZE); + len_processed += MAX_GC_GROUP_NAME_SIZE; + + uint8_t privacy_state; + memcpy(&privacy_state, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + net_unpack_u16(data + len_processed, &shared_state->password_length); + len_processed += sizeof(uint16_t); + memcpy(shared_state->password, data + len_processed, MAX_GC_PASSWORD_SIZE); + len_processed += MAX_GC_PASSWORD_SIZE; + memcpy(shared_state->mod_list_hash, data + len_processed, MOD_MODERATION_HASH_SIZE); + len_processed += MOD_MODERATION_HASH_SIZE; + net_unpack_u32(data + len_processed, &shared_state->topic_lock); + len_processed += sizeof(uint32_t); + + uint8_t voice_state; + memcpy(&voice_state, data + len_processed, sizeof(uint8_t)); + len_processed += sizeof(uint8_t); + + shared_state->voice_state = (Group_Voice_State)voice_state; + shared_state->privacy_state = (Group_Privacy_State)privacy_state; + + return len_processed; +} + +/** @brief Packs topic info into data. + * + * @param data must have room for at least topic length + GC_MIN_PACKED_TOPIC_INFO_SIZE bytes. + * + * Returns packed data length. + */ +non_null() +static uint16_t pack_gc_topic_info(uint8_t *data, uint16_t length, const GC_TopicInfo *topic_info) +{ + if (length < topic_info->length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return 0; + } + + uint16_t packed_len = 0; + + net_pack_u32(data + packed_len, topic_info->version); + packed_len += sizeof(uint32_t); + net_pack_u16(data + packed_len, topic_info->checksum); + packed_len += sizeof(uint16_t); + net_pack_u16(data + packed_len, topic_info->length); + packed_len += sizeof(uint16_t); + memcpy(data + packed_len, topic_info->topic, topic_info->length); + packed_len += topic_info->length; + memcpy(data + packed_len, topic_info->public_sig_key, SIG_PUBLIC_KEY_SIZE); + packed_len += SIG_PUBLIC_KEY_SIZE; + + return packed_len; +} + +/** @brief Unpacks topic info into `topic_info`. + * + * Returns -1 on failure. + * Returns the length of the processed data on success. + */ +non_null() +static int unpack_gc_topic_info(GC_TopicInfo *topic_info, const uint8_t *data, uint16_t length) +{ + if (length < sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t)) { + return -1; + } + + uint16_t len_processed = 0; + + net_unpack_u32(data + len_processed, &topic_info->version); + len_processed += sizeof(uint32_t); + net_unpack_u16(data + len_processed, &topic_info->checksum); + len_processed += sizeof(uint16_t); + net_unpack_u16(data + len_processed, &topic_info->length); + len_processed += sizeof(uint16_t); + + if (topic_info->length > MAX_GC_TOPIC_SIZE) { + topic_info->length = MAX_GC_TOPIC_SIZE; + } + + if (length - len_processed < topic_info->length + SIG_PUBLIC_KEY_SIZE) { + return -1; + } + + if (topic_info->length > 0) { + memcpy(topic_info->topic, data + len_processed, topic_info->length); + len_processed += topic_info->length; + } + + memcpy(topic_info->public_sig_key, data + len_processed, SIG_PUBLIC_KEY_SIZE); + len_processed += SIG_PUBLIC_KEY_SIZE; + + return len_processed; +} + +/** @brief Creates a shared state packet and puts it in data. + * Packet includes self pk hash, shared state signature, and packed shared state info. + * data must have room for at least GC_SHARED_STATE_ENC_PACKET_SIZE bytes. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_shared_state_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ + if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { + return -1; + } + + memcpy(data, chat->shared_state_sig, SIGNATURE_SIZE); + const uint16_t header_len = SIGNATURE_SIZE; + + const uint16_t packed_len = pack_gc_shared_state(data + header_len, length - header_len, &chat->shared_state); + + if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { + return -1; + } + + return (int)(header_len + packed_len); +} + +/** @brief Creates a signature for the group's shared state in packed form. + * + * This function only works for the Founder. + * + * Returns true on success and increments the shared state version. + */ +non_null() +static bool sign_gc_shared_state(GC_Chat *chat) +{ + if (!self_gc_is_founder(chat)) { + LOGGER_ERROR(chat->log, "Failed to sign shared state (invalid permission)"); + return false; + } + + if (chat->shared_state.version != UINT32_MAX) { + /* improbable, but an overflow would break everything */ + set_gc_shared_state_version(chat, chat->shared_state.version + 1); + } else { + LOGGER_WARNING(chat->log, "Shared state version wraparound"); + } + + uint8_t shared_state[GC_PACKED_SHARED_STATE_SIZE]; + const uint16_t packed_len = pack_gc_shared_state(shared_state, sizeof(shared_state), &chat->shared_state); + + if (packed_len != GC_PACKED_SHARED_STATE_SIZE) { + set_gc_shared_state_version(chat, chat->shared_state.version - 1); + LOGGER_ERROR(chat->log, "Failed to pack shared state"); + return false; + } + + const int ret = crypto_sign_detached(chat->shared_state_sig, nullptr, shared_state, packed_len, + get_sig_sk(chat->chat_secret_key)); + + if (ret != 0) { + set_gc_shared_state_version(chat, chat->shared_state.version - 1); + LOGGER_ERROR(chat->log, "Failed to sign shared state (%d)", ret); + return false; + } + + return true; +} + +/** @brief Decrypts data using the shared key associated with `gconn`. + * + * The packet payload should begin with a nonce. + * + * @param message_id should be set to NULL for lossy packets. + * + * Returns length of the plaintext data on success. + * Return -1 if encrypted payload length is invalid. + * Return -2 on decryption failure. + * Return -3 if plaintext payload length is invalid. + */ +non_null(1, 2, 3, 5, 6) nullable(4) +static int group_packet_unwrap(const Logger *log, const GC_Connection *gconn, uint8_t *data, uint64_t *message_id, + uint8_t *packet_type, const uint8_t *packet, uint16_t length) +{ + if (length <= CRYPTO_NONCE_SIZE) { + LOGGER_FATAL(log, "Invalid packet length: %u", length); + return -1; + } + + uint8_t *plain = (uint8_t *)malloc(length); + + if (plain == nullptr) { + LOGGER_ERROR(log, "Failed to allocate memory for plain data buffer"); + return -1; + } + + int plain_len = decrypt_data_symmetric(gconn->session_shared_key, packet, packet + CRYPTO_NONCE_SIZE, + length - CRYPTO_NONCE_SIZE, plain); + + if (plain_len <= 0) { + free(plain); + return plain_len == 0 ? -3 : -2; + } + + const int min_plain_len = message_id != nullptr ? 1 + GC_MESSAGE_ID_BYTES : 1; + + /* remove padding */ + const uint8_t *real_plain = plain; + + while (real_plain[0] == 0) { + ++real_plain; + --plain_len; + + if (plain_len < min_plain_len) { + free(plain); + return -3; + } + } + + uint32_t header_len = sizeof(uint8_t); + *packet_type = real_plain[0]; + plain_len -= sizeof(uint8_t); + + if (message_id != nullptr) { + net_unpack_u64(real_plain + sizeof(uint8_t), message_id); + plain_len -= GC_MESSAGE_ID_BYTES; + header_len += GC_MESSAGE_ID_BYTES; + } + + memcpy(data, real_plain + header_len, plain_len); + + free(plain); + + return plain_len; +} + +int group_packet_wrap( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, + uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, + uint8_t gp_packet_type, uint8_t net_packet_type) +{ + const uint16_t padding_len = group_packet_padding_length(length); + const uint16_t min_packet_size = net_packet_type == NET_PACKET_GC_LOSSLESS + ? length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + GC_MESSAGE_ID_BYTES + 1 + : length + padding_len + CRYPTO_MAC_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + 1; + + if (min_packet_size > packet_size) { + LOGGER_ERROR(log, "Invalid packet buffer size: %u", packet_size); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE) { + LOGGER_ERROR(log, "Packet payload size (%u) exceeds maximum (%u)", length, MAX_GC_PACKET_CHUNK_SIZE); + return -1; + } + + uint8_t *plain = (uint8_t *)malloc(packet_size); + + if (plain == nullptr) { + return -1; + } + + assert(padding_len < packet_size); + + memset(plain, 0, padding_len); + + uint16_t enc_header_len = sizeof(uint8_t); + plain[padding_len] = gp_packet_type; + + if (net_packet_type == NET_PACKET_GC_LOSSLESS) { + net_pack_u64(plain + padding_len + sizeof(uint8_t), message_id); + enc_header_len += GC_MESSAGE_ID_BYTES; + } + + if (length > 0 && data != nullptr) { + memcpy(plain + padding_len + enc_header_len, data, length); + } + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(rng, nonce); + + const uint16_t plain_len = padding_len + enc_header_len + length; + const uint16_t encrypt_buf_size = plain_len + CRYPTO_MAC_SIZE; + + uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + + if (encrypt == nullptr) { + free(plain); + return -2; + } + + const int enc_len = encrypt_data_symmetric(shared_key, nonce, plain, plain_len, encrypt); + + free(plain); + + if (enc_len != encrypt_buf_size) { + LOGGER_ERROR(log, "encryption failed. packet type: 0x%02x, enc_len: %d", gp_packet_type, enc_len); + free(encrypt); + return -3; + } + + packet[0] = net_packet_type; + memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + + free(encrypt); + + return 1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Sends a lossy packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null() +static bool send_lossy_group_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + assert(length <= MAX_GC_PACKET_CHUNK_SIZE); + + if (!gconn->handshaked || gconn->pending_delete) { + return false; + } + + if (data == nullptr || length == 0) { + return false; + } + + const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSY); + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int len = group_packet_wrap( + chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, + packet_size, data, length, 0, packet_type, NET_PACKET_GC_LOSSY); + + if (len < 0) { + LOGGER_ERROR(chat->log, "Failed to encrypt packet (type: 0x%02x, error: %d)", packet_type, len); + free(packet); + return false; + } + + const bool ret = gcc_send_packet(chat, gconn, packet, (uint16_t)len); + + free(packet); + + return ret; +} + +/** @brief Sends a lossless packet to peer_number in chat instance. + * + * Returns true on success. + */ +non_null(1, 2) nullable(3) +static bool send_lossless_group_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type) +{ + assert(length <= MAX_GC_PACKET_SIZE); + + if (!gconn->handshaked || gconn->pending_delete) { + return false; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE) { + return gcc_send_lossless_packet_fragments(chat, gconn, data, length, packet_type); + } + + return gcc_send_lossless_packet(chat, gconn, data, length, packet_type) == 0; +} + +/** @brief Sends a group sync request to peer. + * + * Returns true on success or if sync request timeout has not expired. + */ +non_null() +static bool send_gc_sync_request(GC_Chat *chat, GC_Connection *gconn, uint16_t sync_flags) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_sync_request, GC_SYNC_REQUEST_LIMIT)) { + return true; + } + + chat->last_sync_request = mono_time_get(chat->mono_time); + + uint8_t data[(sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE]; + uint16_t length = sizeof(uint16_t); + + net_pack_u16(data, sync_flags); + + if (chat_is_password_protected(chat)) { + net_pack_u16(data + length, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_REQUEST); +} + +/** @brief Sends a sync response packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool send_gc_sync_response(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + return send_lossless_group_packet(chat, gconn, data, length, GP_SYNC_RESPONSE); +} + +non_null() static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type); +non_null() static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Unpacks a sync announce. + * + * If the announced peer is not already in our peer list, we attempt to + * initiate a peer info exchange with them. + * + * Return true on success (whether or not the peer was added). + */ +non_null() +static bool unpack_gc_sync_announce(GC_Chat *chat, const uint8_t *data, const uint16_t length) +{ + GC_Announce announce = {0}; + + const int unpacked_announces = gca_unpack_announces_list(chat->log, data, length, &announce, 1); + + if (unpacked_announces <= 0) { + LOGGER_WARNING(chat->log, "Failed to unpack announces: %d", unpacked_announces); + return false; + } + + if (memcmp(announce.peer_public_key, chat->self_public_key, ENC_PUBLIC_KEY_SIZE) == 0) { + LOGGER_WARNING(chat->log, "Attempted to unpack our own announce"); + return true; + } + + if (!gca_is_valid_announce(&announce)) { + LOGGER_WARNING(chat->log, "got invalid announce"); + return false; + } + + const IP_Port *ip_port = announce.ip_port_is_set ? &announce.ip_port : nullptr; + const int new_peer_number = peer_add(chat, ip_port, announce.peer_public_key); + + if (new_peer_number == -1) { + LOGGER_ERROR(chat->log, "peer_add() failed"); + return false; + } + + if (new_peer_number == -2) { // peer already added + return true; + } + + if (new_peer_number > 0) { + GC_Connection *new_gconn = get_gc_connection(chat, new_peer_number); + + if (new_gconn == nullptr) { + return false; + } + + uint32_t added_tcp_relays = 0; + + for (uint8_t i = 0; i < announce.tcp_relays_count; ++i) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, new_gconn->tcp_connection_num, + &announce.tcp_relays[i].ip_port, + announce.tcp_relays[i].public_key); + + if (add_tcp_result == -1) { + continue; + } + + if (gcc_save_tcp_relay(chat->rng, new_gconn, &announce.tcp_relays[i]) == 0) { + ++added_tcp_relays; + } + } + + if (!announce.ip_port_is_set && added_tcp_relays == 0) { + gcc_mark_for_deletion(new_gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + LOGGER_ERROR(chat->log, "Sync error: Invalid peer connection info"); + return false; + } + + new_gconn->pending_handshake_type = HS_PEER_INFO_EXCHANGE; + + return true; + } + + LOGGER_FATAL(chat->log, "got impossible return value %d", new_peer_number); + + return false; +} + +/** @brief Handles a sync response packet. + * + * Note: This function may change peer numbers. + * + * Return 0 on success. + * Return -1 if the group is full or the peer failed to unpack. + * Return -2 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_sync_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (chat->connection_state == CS_CONNECTED && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -1; + } + + if (length > 0) { + if (!unpack_gc_sync_announce(chat, data, length)) { + return -1; + } + } + + chat->connection_state = CS_CONNECTED; + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -2; + } + + if (!send_gc_peer_exchange(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send peer exchange on sync response"); + } + + if (c->self_join != nullptr && chat->time_connected == 0) { + c->self_join(c->messenger, chat->group_number, userdata); + chat->time_connected = mono_time_get(chat->mono_time); + } + + return 0; +} + +non_null() static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key); +non_null() static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn); +non_null() static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Creates a sync announce for peer designated by `gconn` and puts it in `announce`, which + * must be zeroed by the caller. + * + * Returns true if announce was successfully created. + */ +non_null() +static bool create_sync_announce(const GC_Chat *chat, const GC_Connection *gconn, uint32_t peer_number, + GC_Announce *announce) +{ + if (chat == nullptr || gconn == nullptr) { + return false; + } + + if (gconn->tcp_relays_count > 0) { + if (gcc_copy_tcp_relay(chat->rng, &announce->tcp_relays[0], gconn)) { + announce->tcp_relays_count = 1; + } + } + + get_gc_peer_public_key(chat, peer_number, announce->peer_public_key); + + if (gcc_ip_port_is_set(gconn)) { + announce->ip_port = gconn->addr.ip_port; + announce->ip_port_is_set = true; + } + + return true; +} + +non_null() +static bool sync_response_send_peers(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, bool first_sync) +{ + // Always respond to a peer's first sync request + if (!first_sync && !mono_time_is_timeout(chat->mono_time, + chat->last_sync_response_peer_list, + GC_SYNC_RESPONSE_PEER_LIST_LIMIT)) { + return true; + } + + uint8_t *response = (uint8_t *)malloc(MAX_GC_PACKET_CHUNK_SIZE); + + if (response == nullptr) { + return false; + } + + size_t num_announces = 0; + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *peer_gconn = get_gc_connection(chat, i); + + if (peer_gconn == nullptr || !peer_gconn->confirmed) { + continue; + } + + if (peer_gconn->public_key_hash == gconn->public_key_hash || i == peer_number) { + continue; + } + + GC_Announce announce = {0}; + + if (!create_sync_announce(chat, peer_gconn, i, &announce)) { + continue; + } + + const int packed_length = gca_pack_announce(chat->log, response, MAX_GC_PACKET_CHUNK_SIZE, &announce); + + if (packed_length <= 0) { + LOGGER_WARNING(chat->log, "Failed to pack announce: %d", packed_length); + continue; + } + + if (!send_gc_sync_response(chat, gconn, response, packed_length)) { + LOGGER_WARNING(chat->log, "Failed to send peer announce info"); + continue; + } + + ++num_announces; + } + + free(response); + + if (num_announces == 0) { + // we send an empty sync response even if we didn't send any peers as an acknowledgement + if (!send_gc_sync_response(chat, gconn, nullptr, 0)) { + LOGGER_WARNING(chat->log, "Failed to send peer announce info"); + return false; + } + } else { + chat->last_sync_response_peer_list = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Sends group state specified by `sync_flags` peer designated by `peer_number`. + * + * Return true on success. + */ +non_null() +static bool sync_response_send_state(GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + uint16_t sync_flags) +{ + const bool first_sync = gconn->last_sync_response == 0; + + // Do not change the order of these four send calls. See: https://toktok.ltd/spec.html#sync_request-0xf8 + if ((sync_flags & GF_STATE) > 0 && chat->shared_state.version > 0) { + if (!send_peer_shared_state(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send shared state"); + return false; + } + + if (!send_peer_mod_list(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send mod list"); + return false; + } + + if (!send_peer_sanctions_list(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send sanctions list"); + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + if ((sync_flags & GF_TOPIC) > 0 && chat->time_connected > 0 && chat->topic_info.version > 0) { + if (!send_peer_topic(chat, gconn)) { + LOGGER_WARNING(chat->log, "Failed to send topic"); + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + if ((sync_flags & GF_PEERS) > 0) { + if (!sync_response_send_peers(chat, gconn, peer_number, first_sync)) { + return false; + } + + gconn->last_sync_response = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Handles a sync request packet and sends a response containing the peer list. + * + * May send additional group info in separate packets, including the topic, shared state, mod list, + * and sanctions list, if respective sync flags are set. + * + * If the group is password protected the password in the request data must first be verified. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if password is invalid. + * Return -3 if we fail to send a response packet. + * Return -4 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_sync_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -4; + } + + if (length < sizeof(uint16_t)) { + return -1; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (chat->shared_state.version == 0) { + LOGGER_DEBUG(chat->log, "Got sync request with uninitialized state"); + return 0; + } + + if (!mono_time_is_timeout(chat->mono_time, gconn->last_sync_response, GC_PING_TIMEOUT)) { + LOGGER_DEBUG(chat->log, "sync request rate limit for peer %d", peer_number); + return 0; + } + + uint16_t sync_flags; + net_unpack_u16(data, &sync_flags); + + if (chat_is_password_protected(chat)) { + if (length < (sizeof(uint16_t) * 2) + MAX_GC_PASSWORD_SIZE) { + return -2; + } + + uint16_t password_length; + net_unpack_u16(data + sizeof(uint16_t), &password_length); + + const uint8_t *password = data + (sizeof(uint16_t) * 2); + + if (!validate_password(chat, password, password_length)) { + LOGGER_DEBUG(chat->log, "Invalid password"); + return -2; + } + } + + if (!sync_response_send_state(chat, gconn, peer_number, sync_flags)) { + return -3; + } + + return 0; +} + +non_null() static void copy_self(const GC_Chat *chat, GC_Peer *peer); +non_null() static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn); + + +/** @brief Shares our TCP relays with peer and adds shared relays to our connection with them. + * + * Returns true on success or if we're not connected to any TCP relays. + */ +non_null() +static bool send_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn) +{ + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + uint8_t data[GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6]; + + const uint32_t n = tcp_copy_connected_relays_index(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, + gconn->tcp_relay_share_index); + + if (n == 0) { + return true; + } + + gconn->tcp_relay_share_index += GCC_MAX_TCP_SHARED_RELAYS; + + for (uint32_t i = 0; i < n; ++i) { + add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_relays[i].ip_port, + tcp_relays[i].public_key); + } + + const int nodes_len = pack_nodes(chat->log, data, sizeof(data), tcp_relays, n); + + if (nodes_len <= 0 || (uint32_t)nodes_len > sizeof(data)) { + LOGGER_ERROR(chat->log, "Failed to pack tcp relays (nodes_len: %d)", nodes_len); + return false; + } + + if (!send_lossless_group_packet(chat, gconn, data, (uint16_t)nodes_len, GP_TCP_RELAYS)) { + LOGGER_ERROR(chat->log, "Failed to send tcp relays"); + return false; + } + + return true; +} + +/** @brief Adds a peer's shared TCP relays to our connection with them. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contains invalid data. + */ +non_null() +static int handle_gc_tcp_relays(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length == 0) { + return -1; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, nullptr, data, length, true); + + if (num_nodes <= 0) { + return -2; + } + + for (int i = 0; i < num_nodes; ++i) { + const Node_format *tcp_node = &tcp_relays[i]; + + if (add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, &tcp_node->ip_port, + tcp_node->public_key) == 0) { + gcc_save_tcp_relay(chat->rng, gconn, tcp_node); + + if (gconn->tcp_relays_count == 1) { + add_gc_saved_peers(chat, gconn); // make sure we save at least one tcp relay + } + } + } + + return 0; +} + +/** @brief Send invite request to peer_number. + * + * If the group requires a password, the packet will + * contain the password supplied by the invite requestor. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_request(const GC_Chat *chat, GC_Connection *gconn) +{ + uint16_t length = 0; + uint8_t data[sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE]; + + if (chat_is_password_protected(chat)) { + net_pack_u16(data, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + length, chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + return send_lossless_group_packet(chat, gconn, data, length, GP_INVITE_REQUEST); +} + +non_null() +static bool send_gc_invite_response(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_INVITE_RESPONSE); +} + +/** @brief Handles an invite response packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if we fail to send a sync request. + */ +non_null() +static int handle_gc_invite_response(GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t flags = GF_PEERS | GF_TOPIC | GF_STATE; + + if (!send_gc_sync_request(chat, gconn, flags)) { + return -1; + } + + return 0; +} + +/** + * @brief Handles an invite response reject packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_invite_response_reject(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, + void *userdata) +{ + if (length < sizeof(uint8_t)) { + return -1; + } + + if (chat->connection_state == CS_CONNECTED) { + return 0; + } + + if (gc_get_self_role(chat) == GR_FOUNDER) { + return 0; + } + + uint8_t type = data[0]; + + if (type >= GJ_INVALID) { + type = GJ_INVITE_FAILED; + } + + chat->connection_state = CS_DISCONNECTED; + + if (c->rejected != nullptr) { + c->rejected(c->messenger, chat->group_number, type, userdata); + } + + return 0; +} + +/** @brief Sends an invite response rejection packet to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_response_reject(const GC_Chat *chat, const GC_Connection *gconn, uint8_t type) +{ + if (type >= GJ_INVALID) { + type = GJ_INVITE_FAILED; + } + + uint8_t data[1]; + data[0] = type; + const uint16_t length = 1; + + return send_lossy_group_packet(chat, gconn, data, length, GP_INVITE_RESPONSE_REJECT); +} + +/** @brief Handles an invite request and verifies that the correct password has been supplied + * if the group is password protected. + * + * Return 0 if invite request is successfully handled. + * Return -1 if the group is full. + * Return -2 if the supplied password is invalid. + * Return -3 if we fail to send an invite response. + * Return -4 if peer_number does not designate a valid peer. + */ +non_null() +static int handle_gc_invite_request(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length) +{ + if (chat->shared_state.version == 0) { // we aren't synced yet; ignore request + return 0; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -4; + } + + int ret = -1; + + uint8_t invite_error; + + if (get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers && !peer_is_founder(chat, peer_number)) { + invite_error = GJ_GROUP_FULL; + goto FAILED_INVITE; + } + + if (chat_is_password_protected(chat)) { + invite_error = GJ_INVALID_PASSWORD; + ret = -2; + + if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { + goto FAILED_INVITE; + } + + uint16_t password_length; + net_unpack_u16(data, &password_length); + + const uint8_t *password = data + sizeof(uint16_t); + + if (!validate_password(chat, password, password_length)) { + goto FAILED_INVITE; + } + } + + if (!send_gc_invite_response(chat, gconn)) { + return -3; + } + + return 0; + +FAILED_INVITE: + send_gc_invite_response_reject(chat, gconn, invite_error); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + + return ret; +} + +/** @brief Sends a lossless packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossless_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + send_lossless_group_packet(chat, gconn, data, length, type); + } + } +} + +/** @brief Sends a lossy packet of type and length to all confirmed peers. */ +non_null() +static void send_gc_lossy_packet_all_peers(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t type) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + + assert(gconn != nullptr); + + if (gconn->confirmed) { + send_lossy_group_packet(chat, gconn, data, length, type); + } + } +} + +/** @brief Creates packet with broadcast header info followed by data of length. + * + * Returns length of packet including header. + */ +non_null(3) nullable(1) +static uint16_t make_gc_broadcast_header(const uint8_t *data, uint16_t length, uint8_t *packet, uint8_t bc_type) +{ + packet[0] = bc_type; + const uint16_t header_len = sizeof(uint8_t); + + if (data != nullptr && length > 0) { + memcpy(packet + header_len, data, length); + } + + return length + header_len; +} + +/** @brief sends a group broadcast packet to all confirmed peers. + * + * Returns true on success. + */ +non_null(1) nullable(2) +static bool send_gc_broadcast_message(const GC_Chat *chat, const uint8_t *data, uint16_t length, uint8_t bc_type) +{ + if (length + GC_BROADCAST_ENC_HEADER_SIZE > MAX_GC_PACKET_SIZE) { + LOGGER_ERROR(chat->log, "Failed to broadcast message: invalid length %u", length); + return false; + } + + uint8_t *packet = (uint8_t *)malloc(length + GC_BROADCAST_ENC_HEADER_SIZE); + + if (packet == nullptr) { + return false; + } + + const uint16_t packet_len = make_gc_broadcast_header(data, length, packet, bc_type); + + send_gc_lossless_packet_all_peers(chat, packet, packet_len, GP_BROADCAST); + + free(packet); + + return true; +} + +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat); + +/** @brief Compares the supplied values with our own state and returns the appropriate + * sync flags for a sync request. + */ +non_null() +static uint16_t get_sync_flags(const GC_Chat *chat, uint16_t peers_checksum, uint16_t peer_count, + uint32_t sstate_version, uint32_t screds_version, uint16_t roles_checksum, + uint32_t topic_version, uint16_t topic_checksum) +{ + uint16_t sync_flags = 0; + + if (peers_checksum != chat->peers_checksum && peer_count >= get_gc_confirmed_numpeers(chat)) { + sync_flags |= GF_PEERS; + } + + if (sstate_version > 0) { + const uint16_t self_roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; + + if ((sstate_version > chat->shared_state.version || screds_version > chat->moderation.sanctions_creds.version) + || (screds_version == chat->moderation.sanctions_creds.version + && roles_checksum != self_roles_checksum)) { + sync_flags |= GF_STATE; + } + } + + if (group_topic_lock_enabled(chat)) { + if (topic_version > chat->topic_info.version || + (topic_version == chat->topic_info.version && topic_checksum > chat->topic_info.checksum)) { + sync_flags |= GF_TOPIC; + } + } else if (topic_checksum > chat->topic_info.checksum) { + sync_flags |= GF_TOPIC; + } + + return sync_flags; +} + +/** @brief Compares a peer's group sync info that we received in a ping packet to our own. + * + * If their info appears to be more recent than ours we send them a sync request. + * + * This function should only be called from `handle_gc_ping()`. + * + * Returns true if a sync request packet is successfully sent. + */ +non_null() +static bool do_gc_peer_state_sync(GC_Chat *chat, GC_Connection *gconn, const uint8_t *sync_data, + const uint16_t length) +{ + if (length < GC_PING_PACKET_MIN_DATA_SIZE) { + return false; + } + + uint16_t peers_checksum; + uint16_t peer_count; + uint32_t sstate_version; + uint32_t screds_version; + uint16_t roles_checksum; + uint32_t topic_version; + uint16_t topic_checksum; + + size_t unpacked_len = 0; + + net_unpack_u16(sync_data, &peers_checksum); + unpacked_len += sizeof(uint16_t); + + net_unpack_u16(sync_data + unpacked_len, &peer_count); + unpacked_len += sizeof(uint16_t); + + net_unpack_u32(sync_data + unpacked_len, &sstate_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u32(sync_data + unpacked_len, &screds_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u16(sync_data + unpacked_len, &roles_checksum); + unpacked_len += sizeof(uint16_t); + + net_unpack_u32(sync_data + unpacked_len, &topic_version); + unpacked_len += sizeof(uint32_t); + + net_unpack_u16(sync_data + unpacked_len, &topic_checksum); + unpacked_len += sizeof(uint16_t); + + if (unpacked_len != GC_PING_PACKET_MIN_DATA_SIZE) { + LOGGER_FATAL(chat->log, "Unpacked length is impossible (%zu)", unpacked_len); + return false; + } + + const uint16_t sync_flags = get_sync_flags(chat, peers_checksum, peer_count, sstate_version, screds_version, + roles_checksum, topic_version, topic_checksum); + + if (sync_flags > 0) { + return send_gc_sync_request(chat, gconn, sync_flags); + } + + return false; +} + +/** @brief Handles a ping packet. + * + * The packet contains sync information including peer's peer list checksum, + * shared state version, topic version, and sanction credentials version. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size or peer is not confirmed. + */ +non_null() +static int handle_gc_ping(GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < GC_PING_PACKET_MIN_DATA_SIZE) { + return -1; + } + + if (!gconn->confirmed) { + return -1; + } + + do_gc_peer_state_sync(chat, gconn, data, length); + + if (length > GC_PING_PACKET_MIN_DATA_SIZE) { + IP_Port ip_port; + memset(&ip_port, 0, sizeof(IP_Port)); + + if (unpack_ip_port(&ip_port, data + GC_PING_PACKET_MIN_DATA_SIZE, + length - GC_PING_PACKET_MIN_DATA_SIZE, false) > 0) { + gcc_set_ip_port(gconn, &ip_port); + add_gc_saved_peers(chat, gconn); + } + } + + return 0; +} + +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status) +{ + const GC_Session *c = m->group_handler; + const GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + self_gc_set_status(chat, status); + + uint8_t data[1]; + data[0] = gc_get_self_status(chat); + + if (!send_gc_broadcast_message(chat, data, 1, GM_STATUS)) { + return -2; + } + + return 0; +} + +/** @brief Handles a status broadcast from `peer`. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid length. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_status(const GC_Session *c, const GC_Chat *chat, GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < sizeof(uint8_t)) { + return -1; + } + + const Group_Peer_Status status = (Group_Peer_Status)data[0]; + + if (status > GS_BUSY) { + LOGGER_WARNING(chat->log, "Received invalid status %u", status); + return 0; + } + + peer->status = status; + + if (c->status_change != nullptr) { + c->status_change(c->messenger, chat->group_number, peer->peer_id, status, userdata); + } + + return 0; +} + +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return UINT8_MAX; + } + + return peer->status; +} + +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return UINT8_MAX; + } + + return peer->role; +} + +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest) +{ + if (dest != nullptr) { + memcpy(dest, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + } +} + +/** @brief Sends self peer info to `gconn`. + * + * If the group is password protected the request will contain the group + * password, which the recipient will validate in the respective + * group message handler. + * + * Returns true on success. + */ +non_null() +static bool send_self_to_peer(const GC_Chat *chat, GC_Connection *gconn) +{ + GC_Peer *self = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + + if (self == nullptr) { + return false; + } + + copy_self(chat, self); + + const uint16_t data_size = PACKED_GC_PEER_SIZE + sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE; + uint8_t *data = (uint8_t *)malloc(data_size); + + if (data == nullptr) { + free(self); + return false; + } + + uint16_t length = 0; + + if (chat_is_password_protected(chat)) { + net_pack_u16(data, chat->shared_state.password_length); + length += sizeof(uint16_t); + + memcpy(data + sizeof(uint16_t), chat->shared_state.password, MAX_GC_PASSWORD_SIZE); + length += MAX_GC_PASSWORD_SIZE; + } + + const int packed_len = pack_gc_peer(data + length, data_size - length, self); + length += packed_len; + + free(self); + + if (packed_len <= 0) { + LOGGER_DEBUG(chat->log, "pack_gc_peer failed in handle_gc_peer_info_request_request %d", packed_len); + free(data); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, data, length, GP_PEER_INFO_RESPONSE); + + free(data); + + return ret; +} + +/** @brief Handles a peer info request packet. + * + * Return 0 on success. + * Return -1 if unconfirmed peer is trying to join a full group. + * Return -2 if response fails. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null() +static int handle_gc_peer_info_request(const GC_Chat *chat, uint32_t peer_number) +{ + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -1; + } + + if (!send_self_to_peer(chat, gconn)) { + return -2; + } + + return 0; +} + +/** @brief Sends a peer info request to peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_peer_info_request(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_PEER_INFO_REQUEST); +} + +/** @brief Do peer info exchange with peer designated by `gconn`. + * + * This function sends two packets to a peer. The first packet is a peer info response containing our own info, + * and the second packet is a peer info request. + * + * Return false if either packet fails to send. + */ +static bool send_gc_peer_exchange(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_self_to_peer(chat, gconn) && send_gc_peer_info_request(chat, gconn); +} + +/** @brief Updates peer's info, validates their group role, and sets them as a confirmed peer. + * If the group is password protected the password must first be validated. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if group number is invalid. + * Return -3 if peer number is invalid. + * Return -4 if unconfirmed peer is trying to join a full group. + * Return -5 if supplied group password is invalid. + * Return -6 if we fail to add the peer to the peer list. + * Return -7 if peer's role cannot be validated. + * Return -8 if malloc fails. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_peer_info_response(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, + const uint8_t *data, uint16_t length, void *userdata) +{ + if (length < PACKED_GC_PEER_SIZE) { + return -1; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -3; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed && get_gc_confirmed_numpeers(chat) >= chat->shared_state.maxpeers + && !peer_is_founder(chat, peer_number)) { + return -4; + } + + uint16_t unpacked_len = 0; + + if (chat_is_password_protected(chat)) { + if (length < sizeof(uint16_t) + MAX_GC_PASSWORD_SIZE) { + return -5; + } + + uint16_t password_length; + net_unpack_u16(data, &password_length); + unpacked_len += sizeof(uint16_t); + + if (!validate_password(chat, data + unpacked_len, password_length)) { + return -5; + } + + unpacked_len += MAX_GC_PASSWORD_SIZE; + } + + if (length <= unpacked_len) { + return -1; + } + + GC_Peer *peer_info = (GC_Peer *)calloc(1, sizeof(GC_Peer)); + + if (peer_info == nullptr) { + return -8; + } + + if (unpack_gc_peer(peer_info, data + unpacked_len, length - unpacked_len) == -1) { + LOGGER_ERROR(chat->log, "unpack_gc_peer() failed"); + free(peer_info); + return -6; + } + + if (peer_update(chat, peer_info, peer_number) == -1) { + LOGGER_WARNING(chat->log, "peer_update() failed"); + free(peer_info); + return -6; + } + + free(peer_info); + + const bool was_confirmed = gconn->confirmed; + gconn->confirmed = true; + + update_gc_peer_roles(chat); + + add_gc_saved_peers(chat, gconn); + + set_gc_peerlist_checksum(chat); + + if (c->peer_join != nullptr && !was_confirmed) { + c->peer_join(c->messenger, chat->group_number, peer->peer_id, userdata); + } + + return 0; +} + +/** @brief Sends the group shared state and its signature to peer_number. + * + * Returns true on success. + */ +non_null() +static bool send_peer_shared_state(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat->shared_state.version == 0) { + return false; + } + + uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; + const int length = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + + if (length != GC_SHARED_STATE_ENC_PACKET_SIZE) { + return false; + } + + return send_lossless_group_packet(chat, gconn, packet, (uint16_t)length, GP_SHARED_STATE); +} + +/** @brief Sends the group shared state and signature to all confirmed peers. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_shared_state(const GC_Chat *chat) +{ + uint8_t packet[GC_SHARED_STATE_ENC_PACKET_SIZE]; + const int packet_len = make_gc_shared_state_packet(chat, packet, sizeof(packet)); + + if (packet_len != GC_SHARED_STATE_ENC_PACKET_SIZE) { + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SHARED_STATE); + + return true; +} + +/** @brief Helper function for `do_gc_shared_state_changes()`. + * + * If the privacy state has been set to private, we kill our group's connection to the DHT. + * Otherwise, we create a new connection with the DHT and flag an announcement. + */ +non_null(1, 2) nullable(3) +static void do_privacy_state_change(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + if (is_public_chat(chat)) { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } else { + chat->update_self_announces = true; + chat->join_type = HJ_PUBLIC; + } + } else { + kill_group_friend_connection(c, chat); + cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); + chat->join_type = HJ_PRIVATE; + } + + if (c->privacy_state != nullptr) { + c->privacy_state(c->messenger, chat->group_number, chat->shared_state.privacy_state, userdata); + } +} + +/** + * Compares old_shared_state with the chat instance's current shared state and triggers the + * appropriate callbacks depending on what pieces of state information changed. Also + * handles DHT announcement/removal if the privacy state changed. + * + * The initial retrieval of the shared state on group join will be ignored by this function. + */ +non_null(1, 2, 3) nullable(4) +static void do_gc_shared_state_changes(const GC_Session *c, GC_Chat *chat, const GC_SharedState *old_shared_state, + void *userdata) +{ + /* Max peers changed */ + if (chat->shared_state.maxpeers != old_shared_state->maxpeers && c->peer_limit != nullptr) { + c->peer_limit(c->messenger, chat->group_number, chat->shared_state.maxpeers, userdata); + } + + /* privacy state changed */ + if (chat->shared_state.privacy_state != old_shared_state->privacy_state) { + do_privacy_state_change(c, chat, userdata); + } + + /* password changed */ + if (chat->shared_state.password_length != old_shared_state->password_length + || memcmp(chat->shared_state.password, old_shared_state->password, old_shared_state->password_length) != 0) { + + if (c->password != nullptr) { + c->password(c->messenger, chat->group_number, chat->shared_state.password, + chat->shared_state.password_length, userdata); + } + } + + /* topic lock state changed */ + if (chat->shared_state.topic_lock != old_shared_state->topic_lock && c->topic_lock != nullptr) { + const Group_Topic_Lock lock_state = group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; + c->topic_lock(c->messenger, chat->group_number, lock_state, userdata); + } + + /* voice state changed */ + if (chat->shared_state.voice_state != old_shared_state->voice_state && c->voice_state != nullptr) { + c->voice_state(c->messenger, chat->group_number, chat->shared_state.voice_state, userdata); + } +} + +/** @brief Sends a sync request to a random peer in the group with the specificed sync flags. + * + * Return true on success. + */ +non_null() +static bool send_gc_random_sync_request(GC_Chat *chat, uint16_t sync_flags) +{ + GC_Connection *rand_gconn = random_gc_connection(chat); + + if (rand_gconn == nullptr) { + return false; + } + + return send_gc_sync_request(chat, rand_gconn, sync_flags); +} + +/** @brief Returns true if all shared state values are legal. */ +non_null() +static bool validate_gc_shared_state(const GC_SharedState *state) +{ + return state->maxpeers > 0 + && state->password_length <= MAX_GC_PASSWORD_SIZE + && state->group_name_len > 0 + && state->group_name_len <= MAX_GC_GROUP_NAME_SIZE + && state->privacy_state <= GI_PRIVATE + && state->voice_state <= GV_FOUNDER; +} + +/** @brief Handles a shared state error and attempts to send a sync request to a random peer. + * + * Return 0 if error is currectly handled. + * Return -1 on failure. + */ +non_null() +static int handle_gc_shared_state_error(GC_Chat *chat, GC_Connection *gconn) +{ + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return 0; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (!send_gc_random_sync_request(chat, GF_STATE)) { + return -1; + } + + return 0; +} + +/** @brief Handles a shared state packet and validates the new shared state. + * + * Return 0 if packet is successfully handled. + * Return -1 if packet is invalid and this is not successfully handled. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_shared_state(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < GC_SHARED_STATE_ENC_PACKET_SIZE) { + return handle_gc_shared_state_error(chat, gconn); + } + + const uint8_t *signature = data; + const uint8_t *ss_data = data + SIGNATURE_SIZE; + const uint16_t ss_length = length - SIGNATURE_SIZE; + + if (crypto_sign_verify_detached(signature, ss_data, GC_PACKED_SHARED_STATE_SIZE, + get_sig_pk(chat->chat_public_key)) == -1) { + LOGGER_DEBUG(chat->log, "Failed to validate shared state signature"); + return handle_gc_shared_state_error(chat, gconn); + } + + uint32_t version; + net_unpack_u32(ss_data, &version); // version is the first 4 bytes of shared state data payload + + if (version == 0 || version < chat->shared_state.version) { + LOGGER_DEBUG(chat->log, "Invalid shared state version (got %u, expected >= %u)", + version, chat->shared_state.version); + return 0; + } + + GC_SharedState old_shared_state = chat->shared_state; + GC_SharedState new_shared_state; + + if (unpack_gc_shared_state(&new_shared_state, ss_data, ss_length) == 0) { + LOGGER_WARNING(chat->log, "Failed to unpack shared state"); + return 0; + } + + if (!validate_gc_shared_state(&new_shared_state)) { + LOGGER_WARNING(chat->log, "Failed to validate shared state"); + return 0; + } + + if (chat->shared_state.version == 0) { // init founder public sig key in moderation object + memcpy(chat->moderation.founder_public_sig_key, + get_sig_pk(new_shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); + } + + chat->shared_state = new_shared_state; + + memcpy(chat->shared_state_sig, signature, sizeof(chat->shared_state_sig)); + + set_gc_shared_state_version(chat, chat->shared_state.version); + + do_gc_shared_state_changes(c, chat, &old_shared_state, userdata); + + return 0; +} + +/** @brief Validates `data` containing a moderation list and unpacks it into the + * shared state of `chat`. + * + * Return 1 if data is valid but mod list doesn't match shared state. + * Return 0 if data is valid. + * Return -1 if data is invalid. + */ +non_null() +static int validate_unpack_mod_list(GC_Chat *chat, const uint8_t *data, uint16_t length, uint16_t num_mods) +{ + if (num_mods > MOD_MAX_NUM_MODERATORS) { + return -1; + } + + uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE] = {0}; + + if (length > 0) { + mod_list_get_data_hash(mod_list_hash, data, length); + } + + // we make sure that this mod list's hash matches the one we got in our last shared state update + if (chat->shared_state.version > 0 + && memcmp(mod_list_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE) != 0) { + LOGGER_WARNING(chat->log, "failed to validate mod list hash"); + return 1; + } + + if (mod_list_unpack(&chat->moderation, data, length, num_mods) == -1) { + LOGGER_WARNING(chat->log, "failed to unpack mod list"); + return -1; + } + + return 0; +} + +/** @brief Handles new mod_list and compares its hash against the mod_list_hash in the shared state. + * + * If the new list fails validation, we attempt to send a sync request to a random peer. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if packet contained invalid data or validation failed. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_mod_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, void *userdata) +{ + if (length < sizeof(uint16_t)) { + return -1; + } + + // only the founder can modify the list; the founder can never be out of sync + if (self_gc_is_founder(chat)) { + return 0; + } + + uint16_t num_mods; + net_unpack_u16(data, &num_mods); + + const int unpack_ret = validate_unpack_mod_list(chat, data + sizeof(uint16_t), length - sizeof(uint16_t), num_mods); + + if (unpack_ret == 0) { + update_gc_peer_roles(chat); + + if (chat->connection_state == CS_CONNECTED && c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_MOD, userdata); + } + + return 0; + } + + if (unpack_ret == 1) { + return 0; + } + + // unpack/validation failed: handle error + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return -2; + } + + if (chat->numpeers <= 1) { + return 0; + } + + send_gc_random_sync_request(chat, GF_STATE); + + return 0; +} + +/** @brief Handles a sanctions list validation error and attempts to send a sync request to a random peer. + * + * Return 0 on success. + * Return -1 on failure. + */ +non_null() +static int handle_gc_sanctions_list_error(GC_Chat *chat) +{ + if (chat->moderation.sanctions_creds.version > 0) { + return 0; + } + + if (chat->shared_state.version == 0) { + chat->connection_state = CS_CONNECTING; + return 0; + } + + if (chat->numpeers <= 1) { + return 0; + } + + if (!send_gc_random_sync_request(chat, GF_STATE)) { + return -1; + } + + return 0; +} + +/** @brief Handles a sanctions list packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to gracefully handle a sanctions list error. + * Return -2 if packet has invalid size. + */ +non_null(1, 2, 3) nullable(5) +static int handle_gc_sanctions_list(const GC_Session *c, GC_Chat *chat, const uint8_t *data, uint16_t length, + void *userdata) +{ + if (length < sizeof(uint16_t)) { + return -2; + } + + uint16_t num_sanctions; + net_unpack_u16(data, &num_sanctions); + + if (num_sanctions > MOD_MAX_NUM_SANCTIONS) { + LOGGER_DEBUG(chat->log, "num_sanctions: %u exceeds maximum", num_sanctions); + return handle_gc_sanctions_list_error(chat); + } + + Mod_Sanction_Creds creds; + + Mod_Sanction *sanctions = (Mod_Sanction *)calloc(num_sanctions, sizeof(Mod_Sanction)); + + if (sanctions == nullptr) { + return -1; + } + + const int unpacked_num = sanctions_list_unpack(sanctions, &creds, num_sanctions, data + sizeof(uint16_t), + length - sizeof(uint16_t), nullptr); + + if (unpacked_num != num_sanctions) { + LOGGER_WARNING(chat->log, "Failed to unpack sanctions list: %d", unpacked_num); + free(sanctions); + return handle_gc_sanctions_list_error(chat); + } + + if (!sanctions_list_check_integrity(&chat->moderation, &creds, sanctions, num_sanctions)) { + LOGGER_WARNING(chat->log, "Sanctions list failed integrity check"); + free(sanctions); + return handle_gc_sanctions_list_error(chat); + } + + if (creds.version < chat->moderation.sanctions_creds.version) { + free(sanctions); + return 0; + } + + // this may occur if two mods change the sanctions list at the exact same time + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + free(sanctions); + return 0; + } + + sanctions_list_cleanup(&chat->moderation); + + chat->moderation.sanctions_creds = creds; + chat->moderation.sanctions = sanctions; + chat->moderation.num_sanctions = num_sanctions; + + update_gc_peer_roles(chat); + + if (chat->connection_state == CS_CONNECTED) { + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, (uint32_t) -1, (uint32_t) -1, MV_OBSERVER, userdata); + } + } + + return 0; +} + +/** @brief Makes a mod_list packet. + * + * Returns length of packet data on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_mod_list_packet(const GC_Chat *chat, uint8_t *data, uint32_t maxlen, uint16_t mod_list_size) +{ + if (maxlen < sizeof(uint16_t) + mod_list_size) { + return -1; + } + + net_pack_u16(data, chat->moderation.num_mods); + const uint16_t length = sizeof(uint16_t) + mod_list_size; + + if (mod_list_size > 0) { + uint8_t *packed_mod_list = (uint8_t *)malloc(mod_list_size); + + if (packed_mod_list == nullptr) { + return -1; + } + + mod_list_pack(&chat->moderation, packed_mod_list); + memcpy(data + sizeof(uint16_t), packed_mod_list, mod_list_size); + + free(packed_mod_list); + } + + return length; +} + +/** @brief Sends the moderator list to peer. + * + * Return true on success. + */ +non_null() +static bool send_peer_mod_list(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + const uint16_t length = sizeof(uint16_t) + mod_list_size; + uint8_t *packet = (uint8_t *)malloc(length); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + + if (packet_len != length) { + free(packet); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, packet, length, GP_MOD_LIST); + + free(packet); + + return ret; +} + +/** @brief Makes a sanctions list packet. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_sanctions_list_packet(const GC_Chat *chat, uint8_t *data, uint16_t maxlen) +{ + if (maxlen < sizeof(uint16_t)) { + return -1; + } + + net_pack_u16(data, chat->moderation.num_sanctions); + const uint16_t length = sizeof(uint16_t); + + const int packed_len = sanctions_list_pack(data + length, maxlen - length, chat->moderation.sanctions, + chat->moderation.num_sanctions, &chat->moderation.sanctions_creds); + + if (packed_len < 0) { + return -1; + } + + return (int)(length + packed_len); +} + +/** @brief Sends the sanctions list to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_sanctions_list(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat->moderation.sanctions_creds.version == 0) { + return true; + } + + const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + + sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + + if (packet_len == -1) { + free(packet); + return false; + } + + const bool ret = send_lossless_group_packet(chat, gconn, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + + free(packet); + + return ret; +} + +/** @brief Sends the sanctions list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_sanctions_list(const GC_Chat *chat) +{ + const uint16_t packet_size = MOD_SANCTION_PACKED_SIZE * chat->moderation.num_sanctions + + sizeof(uint16_t) + MOD_SANCTIONS_CREDS_SIZE; + + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_sanctions_list_packet(chat, packet, packet_size); + + if (packet_len == -1) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, (uint16_t)packet_len, GP_SANCTIONS_LIST); + + free(packet); + + return true; +} + +/** @brief Re-signs all sanctions list entries signed by public_sig_key and broadcasts + * the updated sanctions list to all group peers. + * + * Returns true on success. + */ +non_null() +static bool update_gc_sanctions_list(GC_Chat *chat, const uint8_t *public_sig_key) +{ + const uint16_t num_replaced = sanctions_list_replace_sig(&chat->moderation, public_sig_key); + + if (num_replaced == 0) { + return true; + } + + return broadcast_gc_sanctions_list(chat); +} + +/** @brief Sends mod_list to all peers in group. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_mod_list(const GC_Chat *chat) +{ + const uint16_t mod_list_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + const uint16_t length = sizeof(uint16_t) + mod_list_size; + uint8_t *packet = (uint8_t *)malloc(length); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_mod_list_packet(chat, packet, length, mod_list_size); + + if (packet_len != length) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, length, GP_MOD_LIST); + + free(packet); + + return true; +} + +/** @brief Sends a parting signal to the group. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the packet failed to send. + */ +non_null(1) nullable(2) +static int send_gc_self_exit(const GC_Chat *chat, const uint8_t *partmessage, uint16_t length) +{ + if (length > MAX_GC_PART_MESSAGE_SIZE) { + return -1; + } + + if (!send_gc_broadcast_message(chat, partmessage, length, GM_PEER_EXIT)) { + return -2; + } + + return 0; +} + +/** @brief Handles a peer exit broadcast. */ +non_null(1, 2) nullable(3) +static void handle_gc_peer_exit(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length > MAX_GC_PART_MESSAGE_SIZE) { + length = MAX_GC_PART_MESSAGE_SIZE; + } + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_QUIT, data, length); +} + +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length) +{ + const GC_Session *c = m->group_handler; + const GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (length > MAX_GC_NICK_SIZE) { + return -2; + } + + if (length == 0 || nick == nullptr) { + return -3; + } + + if (!self_gc_set_nick(chat, nick, length)) { + return -2; + } + + if (!send_gc_broadcast_message(chat, nick, length, GM_NICK)) { + return -4; + } + + return 0; +} + +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + if (name != nullptr) { + memcpy(name, peer->nick, peer->nick_length); + } + + return true; +} + +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + return peer->nick_length; +} + +/** @brief Handles a nick change broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 on failure. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_nick(const GC_Session *c, GC_Chat *chat, GC_Peer *peer, const uint8_t *nick, + uint16_t length, void *userdata) +{ + /* If this happens malicious behaviour is highly suspect */ + if (length == 0 || length > MAX_GC_NICK_SIZE) { + GC_Connection *gconn = &peer->gconn; + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SYNC_ERR, nullptr, 0); + LOGGER_WARNING(chat->log, "Invalid nick length for nick: %s (%u)", nick, length); + return -1; + } + + memcpy(peer->nick, nick, length); + peer->nick_length = length; + + if (c->nick_change != nullptr) { + c->nick_change(c->messenger, chat->group_number, peer->peer_id, nick, length, userdata); + } + + return 0; +} + +/** @brief Copies peer_number's public key to `public_key`. + * + * Returns 0 on success. + * Returns -1 if peer_number is invalid. + * Returns -2 if `public_key` is null. + */ +non_null() +static int get_gc_peer_public_key(const GC_Chat *chat, uint32_t peer_number, uint8_t *public_key) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (public_key == nullptr) { + return -2; + } + + memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return 0; +} + +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (public_key == nullptr) { + return -2; + } + + memcpy(public_key, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return 0; +} + +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + if (peer_number_is_self(peer_number)) { // we cannot have a connection with ourselves + return 0; + } + + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return 0; + } + + if (gcc_conn_is_direct(chat->mono_time, gconn)) { + return 2; + } + + return 1; +} + +/** @brief Creates a topic packet and puts it in data. + * + * Packet includes the topic, topic length, public signature key of the + * setter, topic version, and the signature. + * + * Returns packet length on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_topic_packet(const GC_Chat *chat, uint8_t *data, uint16_t length) +{ + if (length < SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + memcpy(data, chat->topic_sig, SIGNATURE_SIZE); + uint16_t data_length = SIGNATURE_SIZE; + + const uint16_t packed_len = pack_gc_topic_info(data + data_length, length - data_length, &chat->topic_info); + data_length += packed_len; + + if (packed_len != chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + return data_length; +} + +/** @brief Sends the group topic to peer. + * + * Returns true on success. + */ +non_null() +static bool send_peer_topic(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + + if (packet_len != packet_buf_size) { + free(packet); + return false; + } + + if (!send_lossless_group_packet(chat, gconn, packet, packet_buf_size, GP_TOPIC)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** + * @brief Initiates a session key rotation with peer designated by `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_peer_key_rotation_request(const GC_Chat *chat, GC_Connection *gconn) +{ + // Only the peer closest to the chat_id sends requests. This is to prevent both peers from sending + // requests at the same time and ending up with a different resulting shared key + if (!gconn->self_is_closer) { + // if this peer hasn't sent us a rotation request in a reasonable timeframe we drop their connection + if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT + GC_PING_TIMEOUT)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + } + + return true; + } + + uint8_t packet[1 + ENC_PUBLIC_KEY_SIZE]; + + net_pack_bool(&packet[0], false); // request type + + create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + + // copy new session public key to packet + memcpy(packet + 1, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); + + if (!send_lossless_group_packet(chat, gconn, packet, sizeof(packet), GP_KEY_ROTATION)) { + return false; + } + + gconn->pending_key_rotation_request = true; + + return true; +} + +/** @brief Sends the group topic to all group members. + * + * Returns true on success. + */ +non_null() +static bool broadcast_gc_topic(const GC_Chat *chat) +{ + const uint16_t packet_buf_size = SIGNATURE_SIZE + chat->topic_info.length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packet = (uint8_t *)malloc(packet_buf_size); + + if (packet == nullptr) { + return false; + } + + const int packet_len = make_gc_topic_packet(chat, packet, packet_buf_size); + + if (packet_len != packet_buf_size) { + free(packet); + return false; + } + + send_gc_lossless_packet_all_peers(chat, packet, packet_buf_size, GP_TOPIC); + + free(packet); + + return true; +} + +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length) +{ + if (length > MAX_GC_TOPIC_SIZE) { + return -1; + } + + const bool topic_lock_enabled = group_topic_lock_enabled(chat); + + if (topic_lock_enabled && gc_get_self_role(chat) > GR_MODERATOR) { + return -2; + } + + if (gc_get_self_role(chat) > GR_USER) { + return -2; + } + + const GC_TopicInfo old_topic_info = chat->topic_info; + + uint8_t old_topic_sig[SIGNATURE_SIZE]; + memcpy(old_topic_sig, chat->topic_sig, SIGNATURE_SIZE); + + // TODO(jfreegman): improbable, but an overflow would break topic setting + if (chat->topic_info.version == UINT32_MAX) { + return -3; + } + + // only increment topic version when lock is enabled + if (topic_lock_enabled) { + ++chat->topic_info.version; + } + + chat->topic_info.length = length; + + if (length > 0) { + assert(topic != nullptr); + memcpy(chat->topic_info.topic, topic, length); + } else { + memset(chat->topic_info.topic, 0, sizeof(chat->topic_info.topic)); + } + + memcpy(chat->topic_info.public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + + chat->topic_info.checksum = get_gc_topic_checksum(&chat->topic_info); + + const uint16_t packet_buf_size = length + GC_MIN_PACKED_TOPIC_INFO_SIZE; + uint8_t *packed_topic = (uint8_t *)malloc(packet_buf_size); + + if (packed_topic == nullptr) { + return -3; + } + + int err = -3; + + const uint16_t packed_len = pack_gc_topic_info(packed_topic, packet_buf_size, &chat->topic_info); + + if (packed_len != packet_buf_size) { + goto ON_ERROR; + } + + if (crypto_sign_detached(chat->topic_sig, nullptr, packed_topic, packet_buf_size, + get_sig_sk(chat->self_secret_key)) == -1) { + goto ON_ERROR; + } + + if (!broadcast_gc_topic(chat)) { + err = -4; + goto ON_ERROR; + } + + chat->topic_prev_checksum = old_topic_info.checksum; + chat->topic_time_set = mono_time_get(chat->mono_time); + + free(packed_topic); + return 0; + +ON_ERROR: + chat->topic_info = old_topic_info; + memcpy(chat->topic_sig, old_topic_sig, SIGNATURE_SIZE); + free(packed_topic); + return err; +} + +void gc_get_topic(const GC_Chat *chat, uint8_t *topic) +{ + if (topic != nullptr) { + memcpy(topic, chat->topic_info.topic, chat->topic_info.length); + } +} + +uint16_t gc_get_topic_size(const GC_Chat *chat) +{ + return chat->topic_info.length; +} + +/** + * If public_sig_key is equal to the key of the topic setter, replaces topic credentials + * and re-broadcasts the updated topic info to the group. + * + * Returns true on success + */ +non_null() +static bool update_gc_topic(GC_Chat *chat, const uint8_t *public_sig_key) +{ + if (memcmp(public_sig_key, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) != 0) { + return true; + } + + return gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) == 0; +} + +/** @brief Validates `topic_info`. + * + * Return true if topic info is valid. + */ +non_null() +static bool handle_gc_topic_validate(const GC_Chat *chat, const GC_Peer *peer, const GC_TopicInfo *topic_info, + bool topic_lock_enabled) +{ + if (topic_info->checksum != get_gc_topic_checksum(topic_info)) { + LOGGER_WARNING(chat->log, "received invalid topic checksum"); + return false; + } + + if (topic_lock_enabled) { + if (!mod_list_verify_sig_pk(&chat->moderation, topic_info->public_sig_key)) { + LOGGER_DEBUG(chat->log, "Invalid topic signature (bad credentials)"); + return false; + } + + if (topic_info->version < chat->topic_info.version) { + return false; + } + } else { + uint8_t public_enc_key[ENC_PUBLIC_KEY_SIZE]; + + if (gc_get_enc_pk_from_sig_pk(chat, public_enc_key, topic_info->public_sig_key)) { + if (sanctions_list_is_observer(&chat->moderation, public_enc_key)) { + LOGGER_DEBUG(chat->log, "Invalid topic signature (sanctioned peer attempted to change topic)"); + return false; + } + } + + if (topic_info->version == chat->shared_state.topic_lock) { + // always accept topic on initial connection + if (!mono_time_is_timeout(chat->mono_time, chat->time_connected, GC_PING_TIMEOUT)) { + return true; + } + + if (chat->topic_prev_checksum == topic_info->checksum && + !mono_time_is_timeout(chat->mono_time, chat->topic_time_set, GC_CONFIRMED_PEER_TIMEOUT)) { + LOGGER_DEBUG(chat->log, "Topic reversion (probable sync error)"); + return false; + } + + return true; + } + + // the topic version should never change when the topic lock is disabled except when + // the founder changes the topic prior to enabling the lock + if (!(peer->role == GR_FOUNDER && topic_info->version == chat->shared_state.topic_lock + 1)) { + LOGGER_ERROR(chat->log, "topic version %u differs from topic lock %u", topic_info->version, + chat->shared_state.topic_lock); + return false; + } + } + + return true; +} + +/** @brief Handles a topic packet. + * + * Return 0 if packet is correctly handled. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_topic(const GC_Session *c, GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < SIGNATURE_SIZE + GC_MIN_PACKED_TOPIC_INFO_SIZE) { + return -1; + } + + const uint16_t old_checksum = chat->topic_info.checksum; + + GC_TopicInfo topic_info; + + if (unpack_gc_topic_info(&topic_info, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE) == -1) { + LOGGER_WARNING(chat->log, "failed to unpack topic"); + return 0; + } + + const uint8_t *signature = data; + + if (crypto_sign_verify_detached(signature, data + SIGNATURE_SIZE, length - SIGNATURE_SIZE, + topic_info.public_sig_key) == -1) { + LOGGER_WARNING(chat->log, "failed to verify topic signature"); + return 0; + } + + const bool topic_lock_enabled = group_topic_lock_enabled(chat); + + if (!handle_gc_topic_validate(chat, peer, &topic_info, topic_lock_enabled)) { + return 0; + } + + // prevents sync issues from triggering the callback needlessly + const bool skip_callback = chat->topic_info.length == topic_info.length + && memcmp(chat->topic_info.topic, topic_info.topic, topic_info.length) == 0; + + chat->topic_prev_checksum = old_checksum; + chat->topic_time_set = mono_time_get(chat->mono_time); + chat->topic_info = topic_info; + memcpy(chat->topic_sig, signature, SIGNATURE_SIZE); + + if (!skip_callback && chat->connection_state == CS_CONNECTED && c->topic_change != nullptr) { + const int setter_peer_number = get_peer_number_of_sig_pk(chat, topic_info.public_sig_key); + const uint32_t peer_id = setter_peer_number >= 0 ? chat->group[setter_peer_number].peer_id : 0; + + c->topic_change(c->messenger, chat->group_number, peer_id, topic_info.topic, topic_info.length, userdata); + } + + return 0; +} + +/** @brief Handles a key exchange packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if length is invalid. + * Return -2 if we fail to create a new session keypair. + * Return -3 if response packet fails to send. + */ +non_null() +static int handle_gc_key_exchange(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < 1 + ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + bool is_response; + net_unpack_bool(&data[0], &is_response); + + const uint8_t *sender_public_session_key = data + 1; + + if (is_response) { + if (!gconn->pending_key_rotation_request) { + LOGGER_WARNING(chat->log, "got unsolicited key rotation response from peer %u", gconn->public_key_hash); + return 0; + } + + // now that we have response we can compute our new shared key and begin using it + gcc_make_session_shared_key(gconn, sender_public_session_key); + + gconn->pending_key_rotation_request = false; + + return 0; + } + + // key generation is pretty cpu intensive so we make sure a peer can't DOS us by spamming requests + if (!mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT / 2)) { + return 0; + } + + uint8_t response[1 + ENC_PUBLIC_KEY_SIZE]; + uint8_t new_session_pk[ENC_PUBLIC_KEY_SIZE]; + uint8_t new_session_sk[ENC_SECRET_KEY_SIZE]; + + net_pack_bool(&response[0], true); + + crypto_memlock(new_session_sk, sizeof(new_session_sk)); + + create_gc_session_keypair(chat->log, chat->rng, new_session_pk, new_session_sk); + + memcpy(response + 1, new_session_pk, ENC_PUBLIC_KEY_SIZE); + + if (!send_lossless_group_packet(chat, gconn, response, sizeof(response), GP_KEY_ROTATION)) { + return -3; + } + + // save new keys and compute new shared key AFTER sending response packet with old key + memcpy(gconn->session_public_key, new_session_pk, sizeof(gconn->session_public_key)); + memcpy(gconn->session_secret_key, new_session_sk, sizeof(gconn->session_secret_key)); + + gcc_make_session_shared_key(gconn, sender_public_session_key); + + crypto_memunlock(new_session_sk, sizeof(new_session_sk)); + + gconn->last_key_rotation = mono_time_get(chat->mono_time); + + return 0; +} + +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name) +{ + if (group_name != nullptr) { + memcpy(group_name, chat->shared_state.group_name, chat->shared_state.group_name_len); + } +} + +uint16_t gc_get_group_name_size(const GC_Chat *chat) +{ + return chat->shared_state.group_name_len; +} + +void gc_get_password(const GC_Chat *chat, uint8_t *password) +{ + if (password != nullptr) { + memcpy(password, chat->shared_state.password, chat->shared_state.password_length); + } +} + +uint16_t gc_get_password_size(const GC_Chat *chat) +{ + return chat->shared_state.password_length; +} + +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length) +{ + if (!self_gc_is_founder(chat)) { + return -1; + } + + uint8_t *oldpasswd = nullptr; + const uint16_t oldlen = chat->shared_state.password_length; + + if (oldlen > 0) { + oldpasswd = (uint8_t *)malloc(oldlen); + + if (oldpasswd == nullptr) { + return -4; + } + + memcpy(oldpasswd, chat->shared_state.password, oldlen); + } + + if (!set_gc_password_local(chat, password, password_length)) { + free(oldpasswd); + return -2; + } + + if (!sign_gc_shared_state(chat)) { + set_gc_password_local(chat, oldpasswd, oldlen); + free(oldpasswd); + return -2; + } + + free(oldpasswd); + + if (!broadcast_gc_shared_state(chat)) { + return -3; + } + + return 0; +} + +/** @brief Validates change to moderator list and either adds or removes peer from our moderator list. + * + * Return target's peer number on success. + * Return -1 on packet handle failure. + * Return -2 if target peer is not online. + * Return -3 if target peer is not a valid role (probably indicates sync issues). + * Return -4 on validation failure. + */ +non_null() +static int validate_unpack_gc_set_mod(GC_Chat *chat, uint32_t peer_number, const uint8_t *data, uint16_t length, + bool add_mod) +{ + int target_peer_number; + uint8_t mod_data[MOD_LIST_ENTRY_SIZE]; + + if (add_mod) { + if (length < 1 + MOD_LIST_ENTRY_SIZE) { + return -1; + } + + memcpy(mod_data, data + 1, MOD_MODERATION_HASH_SIZE); + target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + + if (!gc_peer_number_is_valid(chat, target_peer_number)) { + return -2; + } + + const Group_Role target_role = chat->group[target_peer_number].role; + + if (target_role != GR_USER) { + return -3; + } + + if (!mod_list_add_entry(&chat->moderation, mod_data)) { + return -4; + } + } else { + memcpy(mod_data, data + 1, SIG_PUBLIC_KEY_SIZE); + target_peer_number = get_peer_number_of_sig_pk(chat, mod_data); + + if (!gc_peer_number_is_valid(chat, target_peer_number)) { + return -2; + } + + const Group_Role target_role = chat->group[target_peer_number].role; + + if (target_role != GR_MODERATOR) { + return -3; + } + + if (!mod_list_remove_entry(&chat->moderation, mod_data)) { + return -4; + } + } + + update_gc_peer_roles(chat); + + return target_peer_number; +} + +/** @brief Handles a moderator set broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_mod(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < 1 + SIG_PUBLIC_KEY_SIZE) { + return -1; + } + + const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + + if (setter_peer == nullptr) { + return -3; + } + + if (setter_peer->role != GR_FOUNDER) { + return 0; + } + + bool add_mod; + net_unpack_bool(&data[0], &add_mod); + + const int target_peer_number = validate_unpack_gc_set_mod(chat, peer_number, data, length, add_mod); + + if (target_peer_number == -1) { + return -2; + } + + const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer == nullptr) { + return 0; + } + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + add_mod ? MV_MOD : MV_USER, userdata); + } + + return 0; +} + +/** @brief Sends a set mod broadcast to the group. + * + * Return true on success. + */ +non_null() +static bool send_gc_set_mod(const GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ + const uint16_t length = 1 + SIG_PUBLIC_KEY_SIZE; + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + return false; + } + + net_pack_bool(&data[0], add_mod); + + memcpy(data + 1, get_sig_pk(gconn->addr.public_key), SIG_PUBLIC_KEY_SIZE); + + if (!send_gc_broadcast_message(chat, data, length, GM_SET_MOD)) { + free(data); + return false; + } + + free(data); + + return true; +} + +/** + * Adds or removes the peer designated by gconn from moderator list if `add_mod` is true or false respectively. + * Re-signs and re-distributes an updated mod_list hash. + * + * Returns true on success. + */ +non_null() +static bool founder_gc_set_moderator(GC_Chat *chat, const GC_Connection *gconn, bool add_mod) +{ + if (!self_gc_is_founder(chat)) { + return false; + } + + if (add_mod) { + if (chat->moderation.num_mods >= MOD_MAX_NUM_MODERATORS) { + if (!prune_gc_mod_list(chat)) { + return false; + } + } + + if (!mod_list_add_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { + return false; + } + } else { + if (!mod_list_remove_entry(&chat->moderation, get_sig_pk(gconn->addr.public_key))) { + return false; + } + + if (!update_gc_sanctions_list(chat, get_sig_pk(gconn->addr.public_key)) + || !update_gc_topic(chat, get_sig_pk(gconn->addr.public_key))) { + return false; + } + } + + uint8_t old_hash[MOD_MODERATION_HASH_SIZE]; + memcpy(old_hash, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); + + if (!mod_list_make_hash(&chat->moderation, chat->shared_state.mod_list_hash)) { + return false; + } + + if (!sign_gc_shared_state(chat) || !broadcast_gc_shared_state(chat)) { + memcpy(chat->shared_state.mod_list_hash, old_hash, MOD_MODERATION_HASH_SIZE); + return false; + } + + return send_gc_set_mod(chat, gconn, add_mod); +} + +/** @brief Validates `data` containing a change for the sanction list and unpacks it + * into the sanctions list for `chat`. + * + * if `add_obs` is true we're adding an observer to the list. + * + * Return 1 if sanctions list is not modified. + * Return 0 if data is valid and sanctions list is successfully modified. + * Return -1 if data is invalid format. + */ +non_null() +static int validate_unpack_observer_entry(GC_Chat *chat, const uint8_t *data, uint16_t length, + const uint8_t *public_key, bool add_obs) +{ + Mod_Sanction_Creds creds; + + if (add_obs) { + Mod_Sanction sanction; + + if (sanctions_list_unpack(&sanction, &creds, 1, data, length, nullptr) != 1) { + return -1; + } + + // this may occur if two mods change the sanctions list at the exact same time + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + return 1; + } + + if (sanctions_list_entry_exists(&chat->moderation, &sanction) + || !sanctions_list_add_entry(&chat->moderation, &sanction, &creds)) { + return -1; + } + } else { + if (length < MOD_SANCTIONS_CREDS_SIZE) { + return -1; + } + + if (sanctions_creds_unpack(&creds, data) != MOD_SANCTIONS_CREDS_SIZE) { + return -1; + } + + if (creds.version == chat->moderation.sanctions_creds.version + && creds.checksum <= chat->moderation.sanctions_creds.checksum) { + return 1; + } + + if (!sanctions_list_is_observer(&chat->moderation, public_key) + || !sanctions_list_remove_observer(&chat->moderation, public_key, &creds)) { + return 1; + } + } + + return 0; +} + +/** @brief Handles a set observer broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if the packet contains invalid data. + * Return -3 if `peer_number` does not designate a valid peer. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_set_observer(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length <= 1 + EXT_PUBLIC_KEY_SIZE) { + return -1; + } + + const GC_Peer *setter_peer = get_gc_peer(chat, peer_number); + + if (setter_peer == nullptr) { + return -3; + } + + if (setter_peer->role > GR_MODERATOR) { + LOGGER_DEBUG(chat->log, "peer with insufficient permissions tried to modify sanctions list"); + return 0; + } + + bool add_obs; + net_unpack_bool(&data[0], &add_obs); + + const uint8_t *public_key = data + 1; + + const int target_peer_number = get_peer_number_of_enc_pk(chat, public_key, false); + + if (target_peer_number >= 0 && (uint32_t)target_peer_number == peer_number) { + return -2; + } + + const GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer != nullptr) { + if ((add_obs && target_peer->role != GR_USER) || (!add_obs && target_peer->role != GR_OBSERVER)) { + return 0; + } + } + + const int ret = validate_unpack_observer_entry(chat, + data + 1 + EXT_PUBLIC_KEY_SIZE, + length - 1 - EXT_PUBLIC_KEY_SIZE, + public_key, add_obs); + + if (ret == -1) { + return -2; + } + + + if (ret == 1) { + return 0; + } + + update_gc_peer_roles(chat); + + if (target_peer != nullptr) { + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + add_obs ? MV_OBSERVER : MV_USER, userdata); + } + } + + return 0; +} + +/** @brief Broadcasts observer role data to the group. + * + * Returns true on success. + */ +non_null() +static bool send_gc_set_observer(const GC_Chat *chat, const uint8_t *target_ext_pk, const uint8_t *sanction_data, + uint16_t length, bool add_obs) +{ + const uint16_t packet_len = 1 + EXT_PUBLIC_KEY_SIZE + length; + uint8_t *packet = (uint8_t *)malloc(packet_len); + + if (packet == nullptr) { + return false; + } + + net_pack_bool(&packet[0], add_obs); + + memcpy(packet + 1, target_ext_pk, EXT_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + EXT_PUBLIC_KEY_SIZE, sanction_data, length); + + if (!send_gc_broadcast_message(chat, packet, packet_len, GM_SET_OBSERVER)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** @brief Adds or removes peer_number from the observer list if add_obs is true or false respectively. + * Broadcasts this change to the entire group. + * + * Returns true on success. + */ +non_null() +static bool mod_gc_set_observer(GC_Chat *chat, uint32_t peer_number, bool add_obs) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (gc_get_self_role(chat) >= GR_USER) { + return false; + } + + uint8_t sanction_data[MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_CREDS_SIZE]; + uint16_t length = 0; + + if (add_obs) { + if (chat->moderation.num_sanctions >= MOD_MAX_NUM_SANCTIONS) { + if (!prune_gc_sanctions_list(chat)) { + return false; + } + } + + // if sanctioned peer set the topic we need to overwrite his signature and redistribute + // topic info + const int setter_peer_number = get_peer_number_of_sig_pk(chat, chat->topic_info.public_sig_key); + + if (setter_peer_number >= 0 && (uint32_t)setter_peer_number == peer_number) { + if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { + return false; + } + } + + Mod_Sanction sanction; + + if (!sanctions_list_make_entry(&chat->moderation, gconn->addr.public_key, &sanction, SA_OBSERVER)) { + LOGGER_WARNING(chat->log, "sanctions_list_make_entry failed in mod_gc_set_observer"); + return false; + } + + const int packed_len = sanctions_list_pack(sanction_data, sizeof(sanction_data), &sanction, 1, + &chat->moderation.sanctions_creds); + + if (packed_len == -1) { + return false; + } + + length += packed_len; + } else { + if (!sanctions_list_remove_observer(&chat->moderation, gconn->addr.public_key, nullptr)) { + LOGGER_WARNING(chat->log, "failed to remove sanction"); + return false; + } + + const uint16_t packed_len = sanctions_creds_pack(&chat->moderation.sanctions_creds, sanction_data); + + if (packed_len != MOD_SANCTIONS_CREDS_SIZE) { + return false; + } + + length += packed_len; + } + + if (length > sizeof(sanction_data)) { + LOGGER_FATAL(chat->log, "Invalid sanction data length: %u", length); + return false; + } + + update_gc_peer_roles(chat); + + return send_gc_set_observer(chat, gconn->addr.public_key, sanction_data, length, add_obs); +} + +/** @brief Sets the role of `peer_number` to `new_role`. If necessary this function will first + * remove the peer's current role before applying the new one. + * + * Return true on success. + */ +non_null() +static bool apply_new_gc_role(GC_Chat *chat, uint32_t peer_number, Group_Role current_role, Group_Role new_role) +{ + const GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + switch (current_role) { + case GR_MODERATOR: { + if (!founder_gc_set_moderator(chat, gconn, false)) { + return false; + } + + update_gc_peer_roles(chat); + + if (new_role == GR_OBSERVER) { + return mod_gc_set_observer(chat, peer_number, true); + } + + break; + } + + case GR_OBSERVER: { + if (!mod_gc_set_observer(chat, peer_number, false)) { + return false; + } + + update_gc_peer_roles(chat); + + if (new_role == GR_MODERATOR) { + return founder_gc_set_moderator(chat, gconn, true); + } + + break; + } + + case GR_USER: { + if (new_role == GR_MODERATOR) { + return founder_gc_set_moderator(chat, gconn, true); + } else if (new_role == GR_OBSERVER) { + return mod_gc_set_observer(chat, peer_number, true); + } + + break; + } + + case GR_FOUNDER: + + // Intentional fallthrough + default: { + return false; + } + } + + return true; +} + +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + const GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -2; + } + + const GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed) { + return -2; + } + + const Group_Role current_role = peer->role; + + if (new_role == GR_FOUNDER || peer->role == new_role) { + return -4; + } + + if (peer_number_is_self(peer_number)) { + return -6; + } + + if (current_role == GR_FOUNDER || gc_get_self_role(chat) >= GR_USER) { + return -3; + } + + // moderators can't demote moderators or promote peers to moderator + if (!self_gc_is_founder(chat) && (new_role == GR_MODERATOR || current_role == GR_MODERATOR)) { + return -3; + } + + if (!apply_new_gc_role(chat, peer_number, current_role, new_role)) { + return -5; + } + + update_gc_peer_roles(chat); + + return 0; +} + +/** @brief Return true if topic lock is enabled */ +non_null() +static bool group_topic_lock_enabled(const GC_Chat *chat) +{ + return chat->shared_state.topic_lock == GC_TOPIC_LOCK_ENABLED; +} + +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat) +{ + return chat->shared_state.privacy_state; +} + +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat) +{ + return group_topic_lock_enabled(chat) ? TL_ENABLED : TL_DISABLED; +} + +Group_Voice_State gc_get_voice_state(const GC_Chat *chat) +{ + return chat->shared_state.voice_state; +} + +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (new_lock_state > TL_DISABLED) { + return -2; + } + + if (!self_gc_is_founder(chat)) { + return -3; + } + + if (chat->connection_state <= CS_DISCONNECTED) { + return -4; + } + + const Group_Topic_Lock old_lock_state = gc_get_topic_lock_state(chat); + + if (new_lock_state == old_lock_state) { + return 0; + } + + const uint32_t old_topic_lock = chat->shared_state.topic_lock; + + // If we're enabling the lock the founder needs to sign the current topic and re-broadcast + // it with a new version. This needs to happen before we re-broadcast the shared state because + // if it fails we don't want to enable the topic lock with an invalid topic signature or version. + if (new_lock_state == TL_ENABLED) { + chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; + + if (gc_set_topic(chat, chat->topic_info.topic, chat->topic_info.length) != 0) { + chat->shared_state.topic_lock = old_topic_lock; + return -6; + } + } else { + chat->shared_state.topic_lock = chat->topic_info.version; + } + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.topic_lock = old_topic_lock; + return -5; + } + + if (!broadcast_gc_shared_state(chat)) { + return -6; + } + + return 0; +} + +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!self_gc_is_founder(chat)) { + return -2; + } + + if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { + return -3; + } + + const Group_Voice_State old_voice_state = chat->shared_state.voice_state; + + if (new_voice_state == old_voice_state) { + return 0; + } + + chat->shared_state.voice_state = new_voice_state; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.voice_state = old_voice_state; + return -4; + } + + if (!broadcast_gc_shared_state(chat)) { + return -5; + } + + return 0; +} + +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!self_gc_is_founder(chat)) { + return -2; + } + + if (chat->connection_state == CS_DISCONNECTED || chat->connection_state == CS_NONE) { + return -3; + } + + const Group_Privacy_State old_privacy_state = chat->shared_state.privacy_state; + + if (new_privacy_state == old_privacy_state) { + return 0; + } + + chat->shared_state.privacy_state = new_privacy_state; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.privacy_state = old_privacy_state; + return -4; + } + + if (new_privacy_state == GI_PRIVATE) { + cleanup_gca(c->announces_list, get_chat_id(chat->chat_public_key)); + kill_group_friend_connection(c, chat); + chat->join_type = HJ_PRIVATE; + } else { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } else { + chat->update_self_announces = true; + chat->join_type = HJ_PUBLIC; + } + } + + if (!broadcast_gc_shared_state(chat)) { + return -5; + } + + return 0; +} + +uint16_t gc_get_max_peers(const GC_Chat *chat) +{ + return chat->shared_state.maxpeers; +} + +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers) +{ + if (!self_gc_is_founder(chat)) { + return -1; + } + + const uint16_t old_maxpeers = chat->shared_state.maxpeers; + + if (max_peers == chat->shared_state.maxpeers) { + return 0; + } + + chat->shared_state.maxpeers = max_peers; + + if (!sign_gc_shared_state(chat)) { + chat->shared_state.maxpeers = old_maxpeers; + return -2; + } + + if (!broadcast_gc_shared_state(chat)) { + return -3; + } + + return 0; +} + +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, uint32_t *message_id) +{ + if (length > MAX_GC_MESSAGE_SIZE) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + if (type != GC_MESSAGE_TYPE_NORMAL && type != GC_MESSAGE_TYPE_ACTION) { + return -3; + } + + const GC_Peer *self = get_gc_peer(chat, 0); + assert(self != nullptr); + + if (gc_get_self_role(chat) >= GR_OBSERVER || !peer_has_voice(self, chat->shared_state.voice_state)) { + return -4; + } + + const uint8_t packet_type = type == GC_MESSAGE_TYPE_NORMAL ? GM_PLAIN_MESSAGE : GM_ACTION_MESSAGE; + + const uint16_t length_raw = length + GC_MESSAGE_PSEUDO_ID_SIZE; + uint8_t *message_raw = (uint8_t *)malloc(length_raw); + + if (message_raw == nullptr) { + return -5; + } + + const uint32_t pseudo_msg_id = random_u32(chat->rng); + + net_pack_u32(message_raw, pseudo_msg_id); + memcpy(message_raw + GC_MESSAGE_PSEUDO_ID_SIZE, message, length); + + if (!send_gc_broadcast_message(chat, message_raw, length_raw, packet_type)) { + free(message_raw); + return -5; + } + + if (message_id != nullptr) { + *message_id = pseudo_msg_id; + } + + free(message_raw); + return 0; +} + +/** @brief Handles a message broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(7) +static int handle_gc_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, uint8_t type, void *userdata) +{ + if (data == nullptr || length > MAX_GC_MESSAGE_RAW_SIZE || length <= GC_MESSAGE_PSEUDO_ID_SIZE) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER || !peer_has_voice(peer, chat->shared_state.voice_state)) { + return 0; + } + + if (type != GM_PLAIN_MESSAGE && type != GM_ACTION_MESSAGE) { + LOGGER_WARNING(chat->log, "received invalid message type: %u", type); + return 0; + } + + const uint8_t cb_type = (type == GM_PLAIN_MESSAGE) ? MESSAGE_NORMAL : MESSAGE_ACTION; + + uint32_t pseudo_msg_id; + net_unpack_u32(data, &pseudo_msg_id); + + if (c->message != nullptr) { + c->message(c->messenger, chat->group_number, peer->peer_id, cb_type, data + GC_MESSAGE_PSEUDO_ID_SIZE, + length - GC_MESSAGE_PSEUDO_ID_SIZE, pseudo_msg_id, userdata); + } + + return 0; +} + +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, + uint16_t length) +{ + if (length > MAX_GC_MESSAGE_SIZE) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (type > MESSAGE_ACTION) { + return -4; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -5; + } + + uint8_t *message_with_type = (uint8_t *)malloc(length + 1); + + if (message_with_type == nullptr) { + return -6; + } + + message_with_type[0] = type; + memcpy(message_with_type + 1, message, length); + + uint8_t *packet = (uint8_t *)malloc(length + 1 + GC_BROADCAST_ENC_HEADER_SIZE); + + if (packet == nullptr) { + free(message_with_type); + return -6; + } + + const uint16_t packet_len = make_gc_broadcast_header(message_with_type, length + 1, packet, GM_PRIVATE_MESSAGE); + + free(message_with_type); + + if (!send_lossless_group_packet(chat, gconn, packet, packet_len, GP_BROADCAST)) { + free(packet); + return -6; + } + + free(packet); + + return 0; +} + +/** @brief Handles a private message. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_private_message(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (data == nullptr || length > MAX_GC_MESSAGE_SIZE || length <= 1) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + const uint8_t message_type = data[0]; + + if (message_type > MESSAGE_ACTION) { + LOGGER_WARNING(chat->log, "Received invalid private message type: %u", message_type); + return 0; + } + + if (c->private_message != nullptr) { + c->private_message(c->messenger, chat->group_number, peer->peer_id, message_type, data + 1, length - 1, userdata); + } + + return 0; +} + +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, + uint16_t length) +{ + if (length > MAX_GC_CUSTOM_PACKET_SIZE) { + return -1; + } + + if (message == nullptr || length == 0) { + return -2; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -4; + } + + bool ret; + + if (lossless) { + ret = send_lossless_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); + } else { + ret = send_lossy_group_packet(chat, gconn, message, length, GP_CUSTOM_PRIVATE_PACKET); + } + + return ret ? 0 : -5; +} +/** @brief Handles a custom private packet. + * + * @retval 0 if packet is handled correctly. + * @retval -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_custom_private_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, + const uint8_t *data, uint16_t length, void *userdata) +{ + if (data == nullptr || length == 0 || length > MAX_GC_CUSTOM_PACKET_SIZE) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + if (c->custom_private_packet != nullptr) { + c->custom_private_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); + } + + return 0; +} + +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length) +{ + if (length > MAX_GC_CUSTOM_PACKET_SIZE) { + return -1; + } + + if (data == nullptr || length == 0) { + return -2; + } + + if (gc_get_self_role(chat) >= GR_OBSERVER) { + return -3; + } + + if (lossless) { + send_gc_lossless_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); + } else { + send_gc_lossy_packet_all_peers(chat, data, length, GP_CUSTOM_PACKET); + } + + return 0; +} + +/** @brief Handles a custom packet. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_custom_packet(const GC_Session *c, const GC_Chat *chat, const GC_Peer *peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (data == nullptr || length == 0 || length > MAX_GC_CUSTOM_PACKET_SIZE) { + return -1; + } + + if (peer->ignore || peer->role >= GR_OBSERVER) { + return 0; + } + + if (c->custom_packet != nullptr) { + c->custom_packet(c->messenger, chat->group_number, peer->peer_id, data, length, userdata); + } + + return 0; +} + +/** @brief Handles a peer kick broadcast. + * + * Return 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + */ +non_null(1, 2, 3, 4) nullable(6) +static int handle_gc_kick_peer(const GC_Session *c, GC_Chat *chat, const GC_Peer *setter_peer, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + if (setter_peer->role >= GR_USER) { + return 0; + } + + const uint8_t *target_pk = data; + + const int target_peer_number = get_peer_number_of_enc_pk(chat, target_pk, false); + GC_Peer *target_peer = get_gc_peer(chat, target_peer_number); + + if (target_peer != nullptr) { + if (target_peer->role != GR_USER) { + return 0; + } + } + + if (peer_number_is_self(target_peer_number)) { + assert(target_peer != nullptr); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + chat->connection_state = CS_DISCONNECTED; + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, + MV_KICK, userdata); + } + + return 0; + } + + if (target_peer == nullptr) { /** we don't need to/can't kick a peer that isn't in our peerlist */ + return 0; + } + + gcc_mark_for_deletion(&target_peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_KICKED, nullptr, 0); + + if (c->moderation != nullptr) { + c->moderation(c->messenger, chat->group_number, setter_peer->peer_id, target_peer->peer_id, MV_KICK, userdata); + } + + return 0; +} + +/** @brief Sends a packet to instruct all peers to remove gconn from their peerlist. + * + * Returns true on success. + */ +non_null() +static bool send_gc_kick_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ + uint8_t packet[ENC_PUBLIC_KEY_SIZE]; + memcpy(packet, gconn->addr.public_key, ENC_PUBLIC_KEY_SIZE); + + return send_gc_broadcast_message(chat, packet, ENC_PUBLIC_KEY_SIZE, GM_KICK_PEER); +} + +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id) +{ + const GC_Session *c = m->group_handler; + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + if (peer_number_is_self(peer_number)) { + return -6; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -2; + } + + if (gc_get_self_role(chat) >= GR_USER || peer->role == GR_FOUNDER) { + return -3; + } + + if (!self_gc_is_founder(chat) && peer->role == GR_MODERATOR) { + return -3; + } + + if (peer->role == GR_MODERATOR || peer->role == GR_OBSERVER) { + // this first removes peer from any lists they're on and broadcasts new lists to group + if (gc_set_peer_role(c->messenger, chat->group_number, peer_id, GR_USER) < 0) { + return -4; + } + } + + if (!send_gc_kick_peer(chat, &peer->gconn)) { + return -5; + } + + gcc_mark_for_deletion(&peer->gconn, chat->tcp_conn, GC_EXIT_TYPE_NO_CALLBACK, nullptr, 0); + + return 0; +} + +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type) +{ + if (gconn->pending_delete) { + return true; + } + + if (type == GR_ACK_REQ) { + const uint64_t tm = mono_time_get(chat->mono_time); + + if (gconn->last_requested_packet_time == tm) { + return true; + } + + gconn->last_requested_packet_time = tm; + } else if (type != GR_ACK_RECV) { + return false; + } + + uint8_t data[GC_LOSSLESS_ACK_PACKET_SIZE]; + data[0] = (uint8_t) type; + net_pack_u64(data + 1, message_id); + + return send_lossy_group_packet(chat, gconn, data, GC_LOSSLESS_ACK_PACKET_SIZE, GP_MESSAGE_ACK); +} + +/** @brief Handles a lossless message acknowledgement. + * + * If the type is GR_ACK_RECV we remove the packet from our + * send array. If the type is GR_ACK_REQ we re-send the packet + * associated with the requested message_id. + * + * Returns 0 if packet is handled correctly. + * Return -1 if packet has invalid size. + * Return -2 if we failed to handle the ack (may be caused by connection issues). + * Return -3 if we failed to re-send a requested packet. + */ +non_null() +static int handle_gc_message_ack(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length) +{ + if (length < GC_LOSSLESS_ACK_PACKET_SIZE) { + return -1; + } + + uint64_t message_id; + net_unpack_u64(data + 1, &message_id); + + const Group_Message_Ack_Type type = (Group_Message_Ack_Type) data[0]; + + if (type == GR_ACK_RECV) { + if (!gcc_handle_ack(chat->log, gconn, message_id)) { + return -2; + } + + return 0; + } + + if (type != GR_ACK_REQ) { + return 0; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + const uint16_t idx = gcc_get_array_index(message_id); + + /* re-send requested packet */ + if (gconn->send_array[idx].message_id == message_id) { + if (gcc_encrypt_and_send_lossless_packet(chat, gconn, gconn->send_array[idx].data, + gconn->send_array[idx].data_length, + gconn->send_array[idx].message_id, + gconn->send_array[idx].packet_type)) { + gconn->send_array[idx].last_send_try = tm; + LOGGER_DEBUG(chat->log, "Re-sent requested packet %llu", (unsigned long long)message_id); + } else { + return -3; + } + } + + return 0; +} + +/** @brief Sends a handshake response ack to peer. + * + * Return true on success. + */ +non_null() +static bool send_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_lossless_group_packet(chat, gconn, nullptr, 0, GP_HS_RESPONSE_ACK); +} + +/** @brief Handles a handshake response ack. + * + * Return 0 if packet is handled correctly. + * Return -1 if we failed to respond with an invite request. + */ +non_null() +static int handle_gc_hs_response_ack(const GC_Chat *chat, GC_Connection *gconn) +{ + gconn->handshaked = true; // has to be true before we can send a lossless packet + + if (!send_gc_invite_request(chat, gconn)) { + gconn->handshaked = false; + return -1; + } + + return 0; +} + +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore) +{ + const int peer_number = get_peer_number_of_peer_id(chat, peer_id); + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + if (peer_number_is_self(peer_number)) { + return -2; + } + + peer->ignore = ignore; + + return 0; +} + +/** @brief Handles a broadcast packet. + * + * Returns 0 if packet is handled correctly. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(6) +static int handle_gc_broadcast(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, void *userdata) +{ + if (length < GC_BROADCAST_ENC_HEADER_SIZE) { + return -1; + } + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return -1; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->confirmed) { + return -1; + } + + const uint8_t broadcast_type = data[0]; + + const uint16_t m_len = length - 1; + const uint8_t *message = data + 1; + + int ret = 0; + + switch (broadcast_type) { + case GM_STATUS: { + ret = handle_gc_status(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_NICK: { + ret = handle_gc_nick(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_ACTION_MESSAGE: + + // intentional fallthrough + case GM_PLAIN_MESSAGE: { + ret = handle_gc_message(c, chat, peer, message, m_len, broadcast_type, userdata); + break; + } + + case GM_PRIVATE_MESSAGE: { + ret = handle_gc_private_message(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_PEER_EXIT: { + handle_gc_peer_exit(chat, gconn, message, m_len); + ret = 0; + break; + } + + case GM_KICK_PEER: { + ret = handle_gc_kick_peer(c, chat, peer, message, m_len, userdata); + break; + } + + case GM_SET_MOD: { + ret = handle_gc_set_mod(c, chat, peer_number, message, m_len, userdata); + break; + } + + case GM_SET_OBSERVER: { + ret = handle_gc_set_observer(c, chat, peer_number, message, m_len, userdata); + break; + } + + default: { + LOGGER_DEBUG(chat->log, "Received an invalid broadcast type 0x%02x", broadcast_type); + break; + } + } + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Broadcast handle error %d: type: 0x%02x, peernumber: %u", + ret, broadcast_type, peer_number); + return -1; + } + + return 0; +} + +/** @brief Decrypts data of size `length` using self secret key and sender's public key. + * + * The packet payload should begin with a nonce. + * + * Returns length of plaintext data on success. + * Return -1 if length is invalid. + * Return -2 if decryption fails. + */ +non_null() +static int unwrap_group_handshake_packet(const Logger *log, const uint8_t *self_sk, const uint8_t *sender_pk, + uint8_t *plain, size_t plain_size, const uint8_t *packet, uint16_t length) +{ + if (length <= CRYPTO_NONCE_SIZE) { + LOGGER_FATAL(log, "Invalid handshake packet length %u", length); + return -1; + } + + const int plain_len = decrypt_data(sender_pk, self_sk, packet, packet + CRYPTO_NONCE_SIZE, + length - CRYPTO_NONCE_SIZE, plain); + + if (plain_len < 0 || (uint32_t)plain_len != plain_size) { + LOGGER_DEBUG(log, "decrypt handshake request failed: len: %d, size: %zu", plain_len, plain_size); + return -2; + } + + return plain_len; +} + +/** @brief Encrypts data of length using the peer's shared key a new nonce. + * + * Adds plaintext header consisting of: packet identifier, target public encryption key, + * self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if packet size is invalid. + * Return -2 on malloc failure. + * Return -3 if encryption fails. + */ +non_null() +static int wrap_group_handshake_packet( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *self_sk, + const uint8_t *target_pk, uint8_t *packet, uint32_t packet_size, + const uint8_t *data, uint16_t length) +{ + if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { + LOGGER_FATAL(log, "Invalid packet size: %u", packet_size); + return -1; + } + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(rng, nonce); + + const size_t encrypt_buf_size = length + CRYPTO_MAC_SIZE; + uint8_t *encrypt = (uint8_t *)malloc(encrypt_buf_size); + + if (encrypt == nullptr) { + return -2; + } + + const int enc_len = encrypt_data(target_pk, self_sk, nonce, data, length, encrypt); + + if (enc_len < 0 || (size_t)enc_len != encrypt_buf_size) { + LOGGER_ERROR(log, "Failed to encrypt group handshake packet (len: %d)", enc_len); + free(encrypt); + return -3; + } + + packet[0] = NET_PACKET_GC_HANDSHAKE; + memcpy(packet + 1, self_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE, target_pk, ENC_PUBLIC_KEY_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); + memcpy(packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, encrypt, enc_len); + + free(encrypt); + + return 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + enc_len; +} + +/** @brief Makes, wraps and encrypts a group handshake packet (both request and response are the same format). + * + * Packet contains the packet header, handshake type, self public encryption key, self public signature key, + * request type, and a single TCP relay node. + * + * Returns length of encrypted packet on success. + * Returns -1 on failure. + */ +non_null() +static int make_gc_handshake_packet(const GC_Chat *chat, const GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type, uint8_t *packet, size_t packet_size, + const Node_format *node) +{ + if (packet_size != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)) { + LOGGER_FATAL(chat->log, "invalid packet size: %zu", packet_size); + return -1; + } + + if (chat == nullptr || gconn == nullptr || node == nullptr) { + return -1; + } + + uint8_t data[GC_MIN_HS_PACKET_PAYLOAD_SIZE + sizeof(Node_format)]; + + uint16_t length = sizeof(uint8_t); + + data[0] = handshake_type; + memcpy(data + length, gconn->session_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + memcpy(data + length, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + length += SIG_PUBLIC_KEY_SIZE; + memcpy(data + length, &request_type, sizeof(uint8_t)); + length += sizeof(uint8_t); + memcpy(data + length, &join_type, sizeof(uint8_t)); + length += sizeof(uint8_t); + + int nodes_size = pack_nodes(chat->log, data + length, sizeof(Node_format), node, MAX_SENT_GC_NODES); + + if (nodes_size > 0) { + length += nodes_size; + } else { + nodes_size = 0; + } + + const int enc_len = wrap_group_handshake_packet( + chat->log, chat->rng, chat->self_public_key, chat->self_secret_key, + gconn->addr.public_key, packet, (uint16_t)packet_size, data, length); + + if (enc_len != GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + nodes_size) { + LOGGER_WARNING(chat->log, "Failed to wrap handshake packet: %d", enc_len); + return -1; + } + + return enc_len; +} + +/** @brief Sends a handshake packet to `gconn`. + * + * Handshake_type should be GH_REQUEST or GH_RESPONSE. + * + * Returns true on success. + */ +non_null() +static bool send_gc_handshake_packet(const GC_Chat *chat, GC_Connection *gconn, uint8_t handshake_type, + uint8_t request_type, uint8_t join_type) +{ + if (gconn == nullptr) { + return false; + } + + Node_format node; + memset(&node, 0, sizeof(node)); + + gcc_copy_tcp_relay(chat->rng, &node, gconn); + + uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; + const int length = make_gc_handshake_packet(chat, gconn, handshake_type, request_type, join_type, packet, + sizeof(packet), &node); + + if (length < 0) { + return false; + } + + const bool try_tcp_fallback = gconn->handshake_attempts % 2 == 1 && gconn->tcp_relays_count > 0; + ++gconn->handshake_attempts; + + int ret = -1; + + if (!try_tcp_fallback && gcc_direct_conn_is_possible(chat, gconn)) { + ret = sendpacket(chat->net, &gconn->addr.ip_port, packet, (uint16_t)length); + } + + if (ret != length && gconn->tcp_relays_count == 0) { + LOGGER_WARNING(chat->log, "UDP handshake failed and no TCP relays to fall back on"); + return false; + } + + // Send a TCP handshake if UDP fails, or if UDP succeeded last time but we never got a response + if (gconn->tcp_relays_count > 0 && (ret != length || try_tcp_fallback)) { + if (send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, (uint16_t)length) == -1) { + LOGGER_DEBUG(chat->log, "Send handshake packet failed. Type 0x%02x", request_type); + return false; + } + } + + if (gconn->is_pending_handshake_response) { + gcc_set_send_message_id(gconn, 3); // handshake response is always second packet + } else { + gcc_set_send_message_id(gconn, 2); // handshake request is always first packet + } + + return true; +} + +/** @brief Sends an out-of-band TCP handshake request packet to `gconn`. + * + * Return true on success. + */ +static bool send_gc_oob_handshake_request(const GC_Chat *chat, const GC_Connection *gconn) +{ + if (gconn == nullptr) { + return false; + } + + Node_format node; + memset(&node, 0, sizeof(node)); + + if (!gcc_copy_tcp_relay(chat->rng, &node, gconn)) { + LOGGER_WARNING(chat->log, "Failed to copy TCP relay"); + return false; + } + + uint8_t packet[GC_MIN_ENCRYPTED_HS_PAYLOAD_SIZE + sizeof(Node_format)]; + const int length = make_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type, + packet, sizeof(packet), &node); + + if (length < 0) { + LOGGER_WARNING(chat->log, "Failed to make handshake packet"); + return false; + } + + return tcp_send_oob_packet_using_relay(chat->tcp_conn, gconn->oob_relay_pk, gconn->addr.public_key, + packet, (uint16_t)length) == 0; +} + +/** @brief Handles a handshake response packet and takes appropriate action depending on the value of request_type. + * + * This function assumes the length has already been validated. + * + * Returns peer_number of new connected peer on success. + * Returns -1 on failure. + */ +non_null() +static int handle_gc_handshake_response(const GC_Chat *chat, const uint8_t *sender_pk, const uint8_t *data, + uint16_t length) +{ + // this should be checked at lower level; this is a redundant defense check. Ideally we should + // guarantee that this can never happen in the future. + if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1) { + LOGGER_FATAL(chat->log, "Invalid handshake response size (%u)", length); + return -1; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + if (peer_number == -1) { + return -1; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + const uint8_t *sender_session_pk = data; + + gcc_make_session_shared_key(gconn, sender_session_pk); + + set_sig_pk(gconn->addr.public_key, data + ENC_PUBLIC_KEY_SIZE); + + gcc_set_recv_message_id(gconn, 2); // handshake response is always second packet + + gconn->handshaked = true; + + send_gc_hs_response_ack(chat, gconn); + + const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + + switch (request_type) { + case HS_INVITE_REQUEST: { + if (!send_gc_invite_request(chat, gconn)) { + return -1; + } + + break; + } + + case HS_PEER_INFO_EXCHANGE: { + if (!send_gc_peer_exchange(chat, gconn)) { + return -1; + } + + break; + } + + default: { + return -1; + } + } + + return peer_number; +} + +/** @brief Sends a handshake response packet of type `request_type` to `gconn`. + * + * Return true on success. + */ +non_null() +static bool send_gc_handshake_response(const GC_Chat *chat, GC_Connection *gconn) +{ + return send_gc_handshake_packet(chat, gconn, GH_RESPONSE, gconn->pending_handshake_type, 0); +} + +/** @brief Handles handshake request packets. + * + * Peer is added to peerlist and a lossless connection is established. + * + * This function assumes the length has already been validated. + * + * Return new peer's peer_number on success. + * Return -1 on failure. + */ +#define GC_NEW_PEER_CONNECTION_LIMIT 10 +non_null(1, 3, 4) nullable(2) +static int handle_gc_handshake_request(GC_Chat *chat, const IP_Port *ipp, const uint8_t *sender_pk, + const uint8_t *data, uint16_t length) +{ + // this should be checked at lower level; this is a redundant defense check. Ideally we should + // guarantee that this can never happen in the future. + if (length < ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1) { + LOGGER_FATAL(chat->log, "Invalid length (%u)", length); + return -1; + } + + if (chat->connection_state <= CS_DISCONNECTED) { + return -1; + } + + if (chat->connection_O_metre >= GC_NEW_PEER_CONNECTION_LIMIT) { + chat->block_handshakes = true; + LOGGER_DEBUG(chat->log, "Handshake overflow. Blocking handshakes."); + return -1; + } + + ++chat->connection_O_metre; + + const uint8_t *public_sig_key = data + ENC_PUBLIC_KEY_SIZE; + + const uint8_t request_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE]; + const uint8_t join_type = data[ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1]; + + int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + const bool is_new_peer = peer_number < 0; + + if (is_new_peer) { + peer_number = peer_add(chat, ipp, sender_pk); + + if (peer_number < 0) { + LOGGER_WARNING(chat->log, "Failed to add peer during handshake request"); + return -1; + } + } else { + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (gconn->handshaked) { + gconn->handshaked = false; + LOGGER_DEBUG(chat->log, "Handshaked peer sent a handshake request"); + return -1; + } + + // peers sent handshake request at same time so the closer peer becomes the requestor + // and ignores the request packet while further peer continues on with the response + if (gconn->self_is_closer) { + return 0; + } + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + gcc_set_ip_port(gconn, ipp); + + Node_format node[GCA_MAX_ANNOUNCED_TCP_RELAYS]; + const int processed = ENC_PUBLIC_KEY_SIZE + SIG_PUBLIC_KEY_SIZE + 1 + 1; + + const int nodes_count = unpack_nodes(node, GCA_MAX_ANNOUNCED_TCP_RELAYS, nullptr, + data + processed, length - processed, true); + + if (nodes_count <= 0 && ipp == nullptr) { + if (is_new_peer) { + LOGGER_WARNING(chat->log, "broken tcp relay for new peer"); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + } + + return -1; + } + + if (nodes_count > 0) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &node->ip_port, node->public_key); + + if (add_tcp_result < 0 && is_new_peer && ipp == nullptr) { + LOGGER_WARNING(chat->log, "broken tcp relay for new peer"); + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + return -1; + } + + if (add_tcp_result == 0) { + gcc_save_tcp_relay(chat->rng, gconn, node); + } + } + + const uint8_t *sender_session_pk = data; + + gcc_make_session_shared_key(gconn, sender_session_pk); + + set_sig_pk(gconn->addr.public_key, public_sig_key); + + if (join_type == HJ_PUBLIC && !is_public_chat(chat)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + return -1; + } + + gcc_set_recv_message_id(gconn, 1); // handshake request is always first packet + + gconn->is_pending_handshake_response = true; + gconn->pending_handshake_type = request_type; + + return peer_number; +} + +/** @brief Handles handshake request and handshake response packets. + * + * Returns the peer_number of the connecting peer on success. + * Returns -1 on failure. + */ +non_null(1, 2, 4) nullable(3, 7) +static int handle_gc_handshake_packet(GC_Chat *chat, const uint8_t *sender_pk, const IP_Port *ipp, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + return -1; + } + + const size_t data_buf_size = length - CRYPTO_NONCE_SIZE - CRYPTO_MAC_SIZE; + uint8_t *data = (uint8_t *)malloc(data_buf_size); + + if (data == nullptr) { + return -1; + } + + const int plain_len = unwrap_group_handshake_packet(chat->log, chat->self_secret_key, sender_pk, data, + data_buf_size, packet, length); + + if (plain_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE) { + LOGGER_DEBUG(chat->log, "Failed to unwrap handshake packet (probably a stale request using an old key)"); + free(data); + return -1; + } + + const uint8_t handshake_type = data[0]; + + const uint8_t *real_data = data + 1; + const uint16_t real_len = (uint16_t)plain_len - 1; + + int peer_number; + + if (handshake_type == GH_REQUEST) { + peer_number = handle_gc_handshake_request(chat, ipp, sender_pk, real_data, real_len); + } else if (handshake_type == GH_RESPONSE) { + peer_number = handle_gc_handshake_response(chat, sender_pk, real_data, real_len); + } else { + free(data); + return -1; + } + + free(data); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -1; + } + + if (direct_conn) { + gconn->last_received_direct_time = mono_time_get(chat->mono_time); + } + + return peer_number; +} + +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, uint8_t packet_type, void *userdata) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + GC_Connection *gconn = &peer->gconn; + + int ret; + + switch (packet_type) { + case GP_BROADCAST: { + ret = handle_gc_broadcast(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_PEER_INFO_REQUEST: { + ret = handle_gc_peer_info_request(chat, peer_number); + break; + } + + case GP_PEER_INFO_RESPONSE: { + ret = handle_gc_peer_info_response(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_SYNC_REQUEST: { + ret = handle_gc_sync_request(chat, peer_number, data, length); + break; + } + + case GP_SYNC_RESPONSE: { + ret = handle_gc_sync_response(c, chat, peer_number, data, length, userdata); + break; + } + + case GP_INVITE_REQUEST: { + ret = handle_gc_invite_request(chat, peer_number, data, length); + break; + } + + case GP_INVITE_RESPONSE: { + ret = handle_gc_invite_response(chat, gconn); + break; + } + + case GP_TOPIC: { + ret = handle_gc_topic(c, chat, peer, data, length, userdata); + break; + } + + case GP_SHARED_STATE: { + ret = handle_gc_shared_state(c, chat, gconn, data, length, userdata); + break; + } + + case GP_MOD_LIST: { + ret = handle_gc_mod_list(c, chat, data, length, userdata); + break; + } + + case GP_SANCTIONS_LIST: { + ret = handle_gc_sanctions_list(c, chat, data, length, userdata); + break; + } + + case GP_HS_RESPONSE_ACK: { + ret = handle_gc_hs_response_ack(chat, gconn); + break; + } + + case GP_TCP_RELAYS: { + ret = handle_gc_tcp_relays(chat, gconn, data, length); + break; + } + + case GP_KEY_ROTATION: { + ret = handle_gc_key_exchange(chat, gconn, data, length); + break; + } + + case GP_CUSTOM_PACKET: { + ret = handle_gc_custom_packet(c, chat, peer, data, length, userdata); + break; + } + + case GP_CUSTOM_PRIVATE_PACKET: { + ret = handle_gc_custom_private_packet(c, chat, peer, data, length, userdata); + break; + } + + default: { + LOGGER_DEBUG(chat->log, "Handling invalid lossless group packet type 0x%02x", packet_type); + return false; + } + } + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Lossless packet handle error %d: type: 0x%02x, peernumber: %d", + ret, packet_type, peer_number); + return false; + } + + peer = get_gc_peer(chat, peer_number); + + if (peer != nullptr) { + peer->gconn.last_requested_packet_time = mono_time_get(chat->mono_time); + } + + return true; +} + +/** @brief Handles a packet fragment. + * + * If the fragment is the last one in a sequence we send an ack. Otherwise we + * store the fragment in the receive array and wait for the next segment. + * + * Segments must be processed in correct sequence, and we cannot handle + * non-fragment packets while a sequence is incomplete. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 4) nullable(5, 9) +static bool handle_gc_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + void *userdata) +{ + if (gconn->last_chunk_id != 0 && message_id != gconn->last_chunk_id + 1) { + return gc_send_message_ack(chat, gconn, gconn->last_chunk_id + 1, GR_ACK_REQ); + } + + if (gconn->last_chunk_id == 0 && message_id != gconn->received_message_id + 1) { + return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); + } + + const int frag_ret = gcc_handle_packet_fragment(c, chat, peer_number, gconn, data, length, packet_type, + message_id, userdata); + + if (frag_ret == -1) { + return false; + } + + if (frag_ret == 0) { + gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + gconn->last_received_packet_time = mono_time_get(chat->mono_time); + + return true; +} + +/** @brief Handles lossless groupchat packets. + * + * This function assumes the length has already been validated. + * + * Returns true if packet is successfully handled. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossless_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_LOSSLESS_PAYLOAD_SIZE) { + return false; + } + + int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + if (gconn->pending_delete) { + return true; + } + + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + LOGGER_DEBUG(chat->log, "Failed to allocate memory for packet data buffer"); + return false; + } + + uint8_t packet_type; + uint64_t message_id; + + const int len = group_packet_unwrap(chat->log, gconn, data, &message_id, &packet_type, packet, length); + + if (len < 0) { + Ip_Ntoa ip_str; + LOGGER_DEBUG(chat->log, "Failed to unwrap lossless packet from %s:%d: %d", + net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), gconn->addr.ip_port.port, len); + free(data); + return false; + } + + if (!gconn->handshaked && (packet_type != GP_HS_RESPONSE_ACK && packet_type != GP_INVITE_REQUEST)) { + LOGGER_DEBUG(chat->log, "Got lossless packet type 0x%02x from unconfirmed peer", packet_type); + free(data); + return false; + } + + const bool is_invite_packet = packet_type == GP_INVITE_REQUEST || packet_type == GP_INVITE_RESPONSE + || packet_type == GP_INVITE_RESPONSE_REJECT; + + if (message_id == 3 && is_invite_packet && gconn->received_message_id <= 1) { + // we missed initial handshake request. Drop this packet and wait for another handshake request. + LOGGER_DEBUG(chat->log, "Missed handshake packet, type: 0x%02x", packet_type); + free(data); + return false; + } + + const int lossless_ret = gcc_handle_received_message(chat->log, chat->mono_time, gconn, data, (uint16_t) len, + packet_type, message_id, direct_conn); + + if (packet_type == GP_INVITE_REQUEST && !gconn->handshaked) { // Both peers sent request at same time + free(data); + return true; + } + + if (lossless_ret < 0) { + LOGGER_DEBUG(chat->log, "failed to handle packet %llu (type: 0x%02x, id: %llu)", + (unsigned long long)message_id, packet_type, (unsigned long long)message_id); + free(data); + return false; + } + + /* Duplicate packet */ + if (lossless_ret == 0) { + free(data); + return gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + /* request missing packet */ + if (lossless_ret == 1) { + LOGGER_TRACE(chat->log, "received out of order packet from peer %u. expected %llu, got %llu", peer_number, + (unsigned long long)gconn->received_message_id + 1, (unsigned long long)message_id); + free(data); + return gc_send_message_ack(chat, gconn, gconn->received_message_id + 1, GR_ACK_REQ); + } + + /* handle packet fragment */ + if (lossless_ret == 3) { + const bool frag_ret = handle_gc_packet_fragment(c, chat, peer_number, gconn, data, (uint16_t)len, packet_type, + message_id, userdata); + free(data); + return frag_ret; + } + + const bool ret = handle_gc_lossless_helper(c, chat, peer_number, data, (uint16_t)len, packet_type, userdata); + + free(data); + + if (!ret) { + return false; + } + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + if (gconn != nullptr && lossless_ret == 2) { + gc_send_message_ack(chat, gconn, message_id, GR_ACK_RECV); + } + + return true; +} + +/** @brief Handles lossy groupchat message packets. + * + * This function assumes the length has already been validated. + * + * Return true if packet is handled successfully. + */ +non_null(1, 2, 3, 4) nullable(7) +static bool handle_gc_lossy_packet(const GC_Session *c, GC_Chat *chat, const uint8_t *sender_pk, + const uint8_t *packet, uint16_t length, bool direct_conn, void *userdata) +{ + if (length < GC_MIN_LOSSY_PAYLOAD_SIZE) { + return false; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + GC_Connection *gconn = &peer->gconn; + + if (!gconn->handshaked || gconn->pending_delete) { + LOGGER_DEBUG(chat->log, "Got lossy packet from invalid peer"); + return false; + } + + uint8_t *data = (uint8_t *)malloc(length); + + if (data == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); + return false; + } + + uint8_t packet_type; + + const int len = group_packet_unwrap(chat->log, gconn, data, nullptr, &packet_type, packet, length); + + if (len <= 0) { + Ip_Ntoa ip_str; + LOGGER_DEBUG(chat->log, "Failed to unwrap lossy packet from %s:%d: %d", + net_ip_ntoa(&gconn->addr.ip_port.ip, &ip_str), gconn->addr.ip_port.port, len); + free(data); + return false; + } + + int ret = -1; + const uint16_t payload_len = (uint16_t)len; + + switch (packet_type) { + case GP_MESSAGE_ACK: { + ret = handle_gc_message_ack(chat, gconn, data, payload_len); + break; + } + + case GP_PING: { + ret = handle_gc_ping(chat, gconn, data, payload_len); + break; + } + + case GP_INVITE_RESPONSE_REJECT: { + ret = handle_gc_invite_response_reject(c, chat, data, payload_len, userdata); + break; + } + + case GP_CUSTOM_PACKET: { + ret = handle_gc_custom_packet(c, chat, peer, data, payload_len, userdata); + break; + } + + case GP_CUSTOM_PRIVATE_PACKET: { + ret = handle_gc_custom_private_packet(c, chat, peer, data, payload_len, userdata); + break; + } + + default: { + LOGGER_WARNING(chat->log, "Warning: handling invalid lossy group packet type 0x%02x", packet_type); + free(data); + return false; + } + } + + free(data); + + if (ret < 0) { + LOGGER_DEBUG(chat->log, "Lossy packet handle error %d: type: 0x%02x, peernumber %d", ret, packet_type, + peer_number); + return false; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + if (direct_conn) { + gconn->last_received_direct_time = tm; + } + + gconn->last_received_packet_time = tm; + + return true; +} + +/** @brief Return true if group is either connected or attempting to connect. */ +non_null() +static bool group_can_handle_packets(const GC_Chat *chat) +{ + const GC_Conn_State state = chat->connection_state; + return state == CS_CONNECTING || state == CS_CONNECTED; +} + +/** @brief Sends a group packet to appropriate handler function. + * + * Returns non-negative value on success. + * Returns -1 on failure. + */ +#define MIN_TCP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 3) nullable(5) +static int handle_gc_tcp_packet(void *object, int id, const uint8_t *packet, uint16_t length, void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= MIN_TCP_PACKET_SIZE) { + LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, + MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { + LOGGER_WARNING(m->log, "Got tcp packet with invalid length: %u (expected %u to %u)", length, + MIN_TCP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_TCP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + const uint8_t packet_type = packet[0]; + + const uint8_t *sender_pk = packet + 1; + + const GC_Session *c = m->group_handler; + GC_Chat *chat = nullptr; + + if (packet_type == NET_PACKET_GC_HANDSHAKE) { + chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + } else { + chat = get_chat_by_id(c, sender_pk); + } + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; + uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; + + switch (packet_type) { + case NET_PACKET_GC_LOSSLESS: { + if (!handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { + return -1; + } + + return 0; + } + + case NET_PACKET_GC_LOSSY: { + if (!handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, false, userdata)) { + return -1; + } + + return 0; + } + + case NET_PACKET_GC_HANDSHAKE: { + // handshake packets have an extra public key in plaintext header + if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { + return -1; + } + + payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; + payload = payload + ENC_PUBLIC_KEY_SIZE; + + return handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata); + } + + default: { + return -1; + } + } +} + +non_null(1, 2, 4) nullable(6) +static int handle_gc_tcp_oob_packet(void *object, const uint8_t *public_key, unsigned int tcp_connections_number, + const uint8_t *packet, uint16_t length, void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= GC_MIN_HS_PACKET_PAYLOAD_SIZE) { + LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, + GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + LOGGER_WARNING(m->log, "Got tcp oob packet with invalid length: %u (expected %u to %u)", length, + GC_MIN_HS_PACKET_PAYLOAD_SIZE, MAX_GC_PACKET_CHUNK_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE); + return -1; + } + + const GC_Session *c = m->group_handler; + GC_Chat *chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t packet_type = packet[0]; + + if (packet_type != NET_PACKET_GC_HANDSHAKE) { + return -1; + } + + const uint8_t *sender_pk = packet + 1; + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE; + const uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE - ENC_PUBLIC_KEY_SIZE; + + if (payload_len < GC_MIN_HS_PACKET_PAYLOAD_SIZE + CRYPTO_MAC_SIZE + CRYPTO_NONCE_SIZE) { + return -1; + } + + if (handle_gc_handshake_packet(chat, sender_pk, nullptr, payload, payload_len, false, userdata) == -1) { + return -1; + } + + return 0; +} + +#define MIN_UDP_PACKET_SIZE (1 + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) +non_null(1, 2, 3) nullable(5) +static int handle_gc_udp_packet(void *object, const IP_Port *ipp, const uint8_t *packet, uint16_t length, + void *userdata) +{ + const Messenger *m = (Messenger *)object; + + if (m == nullptr) { + return -1; + } + + if (length <= MIN_UDP_PACKET_SIZE) { + LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, + MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + if (length > MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE) { + LOGGER_WARNING(m->log, "Got UDP packet with invalid length: %u (expected %u to %u)", length, + MIN_UDP_PACKET_SIZE, MAX_GC_PACKET_CHUNK_SIZE + MIN_UDP_PACKET_SIZE + ENC_PUBLIC_KEY_SIZE); + return -1; + } + + const uint8_t packet_type = packet[0]; + const uint8_t *sender_pk = packet + 1; + + const GC_Session *c = m->group_handler; + GC_Chat *chat = nullptr; + + if (packet_type == NET_PACKET_GC_HANDSHAKE) { + chat = get_chat_by_id(c, packet + 1 + ENC_PUBLIC_KEY_SIZE); + } else { + chat = get_chat_by_id(c, sender_pk); + } + + if (chat == nullptr) { + return -1; + } + + if (!group_can_handle_packets(chat)) { + return -1; + } + + const uint8_t *payload = packet + 1 + ENC_PUBLIC_KEY_SIZE; + uint16_t payload_len = length - 1 - ENC_PUBLIC_KEY_SIZE; + bool ret = false; + + switch (packet_type) { + case NET_PACKET_GC_LOSSLESS: { + ret = handle_gc_lossless_packet(c, chat, sender_pk, payload, payload_len, true, userdata); + break; + } + + case NET_PACKET_GC_LOSSY: { + ret = handle_gc_lossy_packet(c, chat, sender_pk, payload, payload_len, true, userdata); + break; + } + + case NET_PACKET_GC_HANDSHAKE: { + // handshake packets have an extra public key in plaintext header + if (length <= 1 + ENC_PUBLIC_KEY_SIZE + ENC_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE) { + return -1; + } + + payload_len = payload_len - ENC_PUBLIC_KEY_SIZE; + payload = payload + ENC_PUBLIC_KEY_SIZE; + + ret = handle_gc_handshake_packet(chat, sender_pk, ipp, payload, payload_len, true, userdata) != -1; + break; + } + + default: { + return -1; + } + } + + return ret ? 0 : -1; +} + +void gc_callback_message(const Messenger *m, gc_message_cb *function) +{ + GC_Session *c = m->group_handler; + c->message = function; +} + +void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function) +{ + GC_Session *c = m->group_handler; + c->private_message = function; +} + +void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function) +{ + GC_Session *c = m->group_handler; + c->custom_packet = function; +} + +void gc_callback_custom_private_packet(const Messenger *m, gc_custom_private_packet_cb *function) +{ + GC_Session *c = m->group_handler; + c->custom_private_packet = function; +} + +void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function) +{ + GC_Session *c = m->group_handler; + c->moderation = function; +} + +void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->nick_change = function; +} + +void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->status_change = function; +} + +void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function) +{ + GC_Session *c = m->group_handler; + c->topic_change = function; +} + +void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function) +{ + GC_Session *c = m->group_handler; + c->topic_lock = function; +} + +void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function) +{ + GC_Session *c = m->group_handler; + c->voice_state = function; +} + +void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_limit = function; +} + +void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function) +{ + GC_Session *c = m->group_handler; + c->privacy_state = function; +} + +void gc_callback_password(const Messenger *m, gc_password_cb *function) +{ + GC_Session *c = m->group_handler; + c->password = function; +} + +void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_join = function; +} + +void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function) +{ + GC_Session *c = m->group_handler; + c->peer_exit = function; +} + +void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function) +{ + GC_Session *c = m->group_handler; + c->self_join = function; +} + +void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function) +{ + GC_Session *c = m->group_handler; + c->rejected = function; +} + +/** @brief Deletes peer_number from group. + * + * `no_callback` should be set to true if the `peer_exit` callback + * should not be triggered. + * + * Return true on success. + */ +static bool peer_delete(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, void *userdata) +{ + GC_Peer *peer = get_gc_peer(chat, peer_number); + + if (peer == nullptr) { + return false; + } + + // We need to save some peer info for the callback before deleting it + const bool peer_confirmed = peer->gconn.confirmed; + const uint32_t peer_id = peer->peer_id; + uint8_t nick[MAX_GC_NICK_SIZE]; + const uint16_t nick_length = peer->nick_length; + const GC_Exit_Info exit_info = peer->gconn.exit_info; + + assert(nick_length <= MAX_GC_NICK_SIZE); + memcpy(nick, peer->nick, nick_length); + + gcc_peer_cleanup(&peer->gconn); + + --chat->numpeers; + + if (chat->numpeers != peer_number) { + chat->group[peer_number] = chat->group[chat->numpeers]; + } + + chat->group[chat->numpeers] = (GC_Peer) { + 0 + }; + + GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, chat->numpeers * sizeof(GC_Peer)); + + if (tmp_group == nullptr) { + return false; + } + + chat->group = tmp_group; + + set_gc_peerlist_checksum(chat); + + if (peer_confirmed) { + refresh_gc_saved_peers(chat); + } + + if (exit_info.exit_type != GC_EXIT_TYPE_NO_CALLBACK && c->peer_exit != nullptr && peer_confirmed) { + c->peer_exit(c->messenger, chat->group_number, peer_id, exit_info.exit_type, nick, + nick_length, exit_info.part_message, exit_info.length, userdata); + } + + return true; +} + +/** @brief Updates peer_number with info from `peer` and validates peer data. + * + * Returns peer_number on success. + * Returns -1 on failure. + */ +static int peer_update(const GC_Chat *chat, const GC_Peer *peer, uint32_t peer_number) +{ + if (peer->nick_length == 0) { + return -1; + } + + if (peer->status > GS_BUSY) { + return -1; + } + + if (peer->role > GR_OBSERVER) { + return -1; + } + + GC_Peer *curr_peer = get_gc_peer(chat, peer_number); + assert(curr_peer != nullptr); + + curr_peer->status = peer->status; + curr_peer->nick_length = peer->nick_length; + + memcpy(curr_peer->nick, peer->nick, peer->nick_length); + + return peer_number; +} + +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key) +{ + if (get_peer_number_of_enc_pk(chat, public_key, false) != -1) { + return -2; + } + + const uint32_t peer_id = get_new_peer_id(chat); + + if (peer_id == UINT32_MAX) { + LOGGER_WARNING(chat->log, "Failed to add peer: all peer ID's are taken?"); + return -1; + } + + const int peer_number = chat->numpeers; + int tcp_connection_num = -1; + + if (peer_number > 0) { // we don't need a connection to ourself + tcp_connection_num = new_tcp_connection_to(chat->tcp_conn, public_key, 0); + + if (tcp_connection_num == -1) { + LOGGER_WARNING(chat->log, "Failed to init tcp connection for peer %d", peer_number); + } + } + + GC_Message_Array_Entry *send = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); + GC_Message_Array_Entry *recv = (GC_Message_Array_Entry *)calloc(GCC_BUFFER_SIZE, sizeof(GC_Message_Array_Entry)); + + if (send == nullptr || recv == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for gconn buffers"); + + if (tcp_connection_num != -1) { + kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); + } + + free(send); + free(recv); + return -1; + } + + GC_Peer *tmp_group = (GC_Peer *)realloc(chat->group, (chat->numpeers + 1) * sizeof(GC_Peer)); + + if (tmp_group == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for group realloc"); + + if (tcp_connection_num != -1) { + kill_tcp_connection_to(chat->tcp_conn, tcp_connection_num); + } + + free(send); + free(recv); + return -1; + } + + ++chat->numpeers; + chat->group = tmp_group; + + chat->group[peer_number] = (GC_Peer) { + 0 + }; + + GC_Connection *gconn = &chat->group[peer_number].gconn; + + gconn->send_array = send; + gconn->recv_array = recv; + + gcc_set_ip_port(gconn, ipp); + chat->group[peer_number].role = GR_USER; + chat->group[peer_number].peer_id = peer_id; + chat->group[peer_number].ignore = false; + + crypto_memlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); + + create_gc_session_keypair(chat->log, chat->rng, gconn->session_public_key, gconn->session_secret_key); + + if (peer_number > 0) { + memcpy(gconn->addr.public_key, public_key, ENC_PUBLIC_KEY_SIZE); // we get the sig key in the handshake + } else { + memcpy(gconn->addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + gcc_set_send_message_id(gconn, 1); + gconn->public_key_hash = gc_get_pk_jenkins_hash(public_key); + gconn->last_received_packet_time = tm; + gconn->last_key_rotation = tm; + gconn->tcp_connection_num = tcp_connection_num; + gconn->last_sent_ip_time = tm; + gconn->last_sent_ping_time = tm - (GC_PING_TIMEOUT / 2) + (peer_number % (GC_PING_TIMEOUT / 2)); + gconn->self_is_closer = id_closest(get_chat_id(chat->chat_public_key), + get_enc_key(chat->self_public_key), + get_enc_key(gconn->addr.public_key)) == 1; + return peer_number; +} + +/** @brief Copies own peer data to `peer`. */ +non_null() +static void copy_self(const GC_Chat *chat, GC_Peer *peer) +{ + *peer = (GC_Peer) { + 0 + }; + + peer->status = gc_get_self_status(chat); + gc_get_self_nick(chat, peer->nick); + peer->nick_length = gc_get_self_nick_size(chat); + peer->role = gc_get_self_role(chat); +} + +/** @brief Returns true if we haven't received a ping from this peer after n seconds. + * n depends on whether or not the peer has been confirmed. + */ +non_null() +static bool peer_timed_out(const Mono_Time *mono_time, const GC_Connection *gconn) +{ + return mono_time_is_timeout(mono_time, gconn->last_received_packet_time, gconn->confirmed + ? GC_CONFIRMED_PEER_TIMEOUT + : GC_UNCONFIRMED_PEER_TIMEOUT); +} + +/** @brief Attempts to send pending handshake packets to peer designated by `gconn`. + * + * One request of each type can be sent per `GC_SEND_HANDSHAKE_INTERVAL` seconds. + * + * Return true on success. + */ +non_null() +static bool send_pending_handshake(const GC_Chat *chat, GC_Connection *gconn) +{ + if (chat == nullptr || gconn == nullptr) { + return false; + } + + if (gconn->is_pending_handshake_response) { + if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_response, GC_SEND_HANDSHAKE_INTERVAL)) { + return true; + } + + gconn->last_handshake_response = mono_time_get(chat->mono_time); + + return send_gc_handshake_response(chat, gconn); + } + + if (!mono_time_is_timeout(chat->mono_time, gconn->last_handshake_request, GC_SEND_HANDSHAKE_INTERVAL)) { + return true; + } + + gconn->last_handshake_request = mono_time_get(chat->mono_time); + + if (gconn->is_oob_handshake) { + return send_gc_oob_handshake_request(chat, gconn); + } + + return send_gc_handshake_packet(chat, gconn, GH_REQUEST, gconn->pending_handshake_type, chat->join_type); +} + +#define GC_TCP_RELAY_SEND_INTERVAL (60 * 3) +non_null(1, 2) nullable(3) +static void do_peer_connections(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->pending_delete) { + continue; + } + + if (peer_timed_out(chat->mono_time, gconn)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + continue; + } + + gcc_resend_packets(chat, gconn); + + if (gconn->tcp_relays_count > 0 && + mono_time_is_timeout(chat->mono_time, gconn->last_sent_tcp_relays_time, GC_TCP_RELAY_SEND_INTERVAL)) { + if (gconn->confirmed) { + send_gc_tcp_relays(chat, gconn); + gconn->last_sent_tcp_relays_time = mono_time_get(chat->mono_time); + } + } + + gcc_check_recv_array(c, chat, gconn, i, userdata); // may change peer numbers + } +} + +/** @brief Executes pending handshakes for peers. + * + * If our peerlist is empty we periodically try to + * load peers from our saved peers list and initiate handshake requests with them. + */ +#define LOAD_PEERS_TIMEOUT (GC_UNCONFIRMED_PEER_TIMEOUT + 10) +non_null() +static void do_handshakes(GC_Chat *chat) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->handshaked || gconn->pending_delete) { + continue; + } + + send_pending_handshake(chat, gconn); + } + + if (chat->numpeers <= 1) { + const uint64_t tm = mono_time_get(chat->mono_time); + + if (mono_time_is_timeout(chat->mono_time, chat->last_time_peers_loaded, LOAD_PEERS_TIMEOUT)) { + load_gc_peers(chat, chat->saved_peers, GC_MAX_SAVED_PEERS); + chat->last_time_peers_loaded = tm; + } + } +} + +/** @brief Adds `gconn` to the group timeout list. */ +non_null() +static void add_gc_peer_timeout_list(GC_Chat *chat, const GC_Connection *gconn) +{ + const size_t idx = chat->timeout_list_index; + const uint64_t tm = mono_time_get(chat->mono_time); + + copy_gc_saved_peer(chat->rng, gconn, &chat->timeout_list[idx].addr); + + chat->timeout_list[idx].last_seen = tm; + chat->timeout_list[idx].last_reconn_try = 0; + chat->timeout_list_index = (idx + 1) % MAX_GC_SAVED_TIMEOUTS; +} + +non_null(1, 2) nullable(3) +static void do_peer_delete(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (gconn->pending_delete) { + const GC_Exit_Info *exit_info = &gconn->exit_info; + + if (exit_info->exit_type == GC_EXIT_TYPE_TIMEOUT && gconn->confirmed) { + add_gc_peer_timeout_list(chat, gconn); + } + + if (!peer_delete(c, chat, i, userdata)) { + LOGGER_ERROR(chat->log, "Failed to delete peer %u", i); + } + + if (i >= chat->numpeers) { + break; + } + } + } +} + +/** @brief Constructs and sends a ping packet to `gconn` containing info needed for group syncing + * and connection maintenance. + * + * Return true on success. + */ +non_null() +static bool ping_peer(const GC_Chat *chat, const GC_Connection *gconn) +{ + const uint16_t buf_size = GC_PING_PACKET_MIN_DATA_SIZE + sizeof(IP_Port); + uint8_t *data = (uint8_t *)malloc(buf_size); + + if (data == nullptr) { + return false; + } + + const uint16_t roles_checksum = chat->moderation.sanctions_creds.checksum + chat->roles_checksum; + uint16_t packed_len = 0; + + net_pack_u16(data, chat->peers_checksum); + packed_len += sizeof(uint16_t); + + net_pack_u16(data + packed_len, get_gc_confirmed_numpeers(chat)); + packed_len += sizeof(uint16_t); + + net_pack_u32(data + packed_len, chat->shared_state.version); + packed_len += sizeof(uint32_t); + + net_pack_u32(data + packed_len, chat->moderation.sanctions_creds.version); + packed_len += sizeof(uint32_t); + + net_pack_u16(data + packed_len, roles_checksum); + packed_len += sizeof(uint16_t); + + net_pack_u32(data + packed_len, chat->topic_info.version); + packed_len += sizeof(uint32_t); + + net_pack_u16(data + packed_len, chat->topic_info.checksum); + packed_len += sizeof(uint16_t); + + if (packed_len != GC_PING_PACKET_MIN_DATA_SIZE) { + LOGGER_FATAL(chat->log, "Packed length is impossible"); + } + + if (chat->self_udp_status == SELF_UDP_STATUS_WAN && !gcc_conn_is_direct(chat->mono_time, gconn) + && mono_time_is_timeout(chat->mono_time, gconn->last_sent_ip_time, GC_SEND_IP_PORT_INTERVAL)) { + + const int packed_ipp_len = pack_ip_port(chat->log, data + buf_size - sizeof(IP_Port), sizeof(IP_Port), + &chat->self_ip_port); + + if (packed_ipp_len > 0) { + packed_len += packed_ipp_len; + } + } + + if (!send_lossy_group_packet(chat, gconn, data, packed_len, GP_PING)) { + free(data); + return true; + } + + free(data); + + return false; +} + +/** + * Sends a ping packet to peers that haven't been pinged in at least GC_PING_TIMEOUT seconds, and + * a key rotation request to peers with whom we haven't refreshed keys in at least GC_KEY_ROTATION_TIMEOUT + * seconds. + * + * Ping packet always includes your confirmed peer count, a peer list checksum, your shared state and sanctions + * list version for syncing purposes. We also occasionally try to send our own IP info to peers that we + * do not have a direct connection with. + */ +#define GC_DO_PINGS_INTERVAL 2 +non_null() +static void do_gc_ping_and_key_rotation(GC_Chat *chat) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_ping_interval, GC_DO_PINGS_INTERVAL)) { + return; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + if (!gconn->confirmed) { + continue; + } + + if (mono_time_is_timeout(chat->mono_time, gconn->last_sent_ping_time, GC_PING_TIMEOUT)) { + if (ping_peer(chat, gconn)) { + gconn->last_sent_ping_time = tm; + } + } + + if (mono_time_is_timeout(chat->mono_time, gconn->last_key_rotation, GC_KEY_ROTATION_TIMEOUT)) { + if (send_peer_key_rotation_request(chat, gconn)) { + gconn->last_key_rotation = tm; + } + } + } + + chat->last_ping_interval = tm; +} + +non_null() +static void do_new_connection_cooldown(GC_Chat *chat) +{ + if (chat->connection_O_metre == 0) { + return; + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + if (chat->connection_cooldown_timer < tm) { + chat->connection_cooldown_timer = tm; + --chat->connection_O_metre; + + if (chat->connection_O_metre == 0 && chat->block_handshakes) { + chat->block_handshakes = false; + LOGGER_DEBUG(chat->log, "Unblocking handshakes"); + } + } +} + +#define TCP_RELAYS_CHECK_INTERVAL 10 +non_null(1, 2) nullable(3) +static void do_gc_tcp(const GC_Session *c, GC_Chat *chat, void *userdata) +{ + if (chat->tcp_conn == nullptr || !group_can_handle_packets(chat)) { + return; + } + + do_tcp_connections(chat->log, chat->tcp_conn, userdata); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + const GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + const bool tcp_set = !gcc_conn_is_direct(chat->mono_time, gconn); + set_tcp_connection_to_status(chat->tcp_conn, gconn->tcp_connection_num, tcp_set); + } + + if (mono_time_is_timeout(chat->mono_time, chat->last_checked_tcp_relays, TCP_RELAYS_CHECK_INTERVAL) && + tcp_connected_relays_count(chat->tcp_conn) != chat->tcp_connections) { + add_tcp_relays_to_chat(c, chat); + chat->last_checked_tcp_relays = mono_time_get(chat->mono_time); + } +} + +/** + * Updates our TCP and UDP connection status and flags a new announcement if our connection has + * changed and we have either a UDP or TCP connection. + */ +#define GC_SELF_CONNECTION_CHECK_INTERVAL 2 +non_null() +static void do_self_connection(const GC_Session *c, GC_Chat *chat) +{ + if (!mono_time_is_timeout(chat->mono_time, chat->last_self_announce_check, GC_SELF_CONNECTION_CHECK_INTERVAL)) { + return; + } + + const uint32_t tcp_connections = tcp_connected_relays_count(chat->tcp_conn); + + const Messenger *m = c->messenger; + const unsigned int self_udp_status = ipport_self_copy(m->dht, &chat->self_ip_port); + + const bool udp_change = (chat->self_udp_status != self_udp_status) && (self_udp_status != SELF_UDP_STATUS_NONE); + const bool tcp_change = tcp_connections != chat->tcp_connections; + + if (is_public_chat(chat) && (udp_change || tcp_change)) { + chat->update_self_announces = true; + } + + chat->tcp_connections = tcp_connections; + chat->self_udp_status = (Self_UDP_Status) self_udp_status; + chat->last_self_announce_check = mono_time_get(chat->mono_time); +} + +/** @brief Attempts to initiate a new connection with peers in the timeout list. + * + * This function is not used for public groups as the DHT and group sync mechanism + * should automatically do this for us. + */ +#define TIMED_OUT_RECONN_INTERVAL 2 +non_null() +static void do_timed_out_reconn(GC_Chat *chat) +{ + if (is_public_chat(chat)) { + return; + } + + if (!mono_time_is_timeout(chat->mono_time, chat->last_timed_out_reconn_try, TIMED_OUT_RECONN_INTERVAL)) { + return; + } + + const uint64_t curr_time = mono_time_get(chat->mono_time); + + for (size_t i = 0; i < MAX_GC_SAVED_TIMEOUTS; ++i) { + GC_TimedOutPeer *timeout = &chat->timeout_list[i]; + + if (timeout->last_seen == 0 || timeout->last_seen == curr_time) { + continue; + } + + if (mono_time_is_timeout(chat->mono_time, timeout->last_seen, GC_TIMED_OUT_STALE_TIMEOUT) + || get_peer_number_of_enc_pk(chat, timeout->addr.public_key, true) != -1) { + *timeout = (GC_TimedOutPeer) { + {{ + 0 + } + } + }; + continue; + } + + if (mono_time_is_timeout(chat->mono_time, timeout->last_reconn_try, GC_TIMED_OUT_RECONN_TIMEOUT)) { + if (load_gc_peers(chat, &timeout->addr, 1) != 1) { + LOGGER_WARNING(chat->log, "Failed to load timed out peer"); + } + + timeout->last_reconn_try = curr_time; + } + } + + chat->last_timed_out_reconn_try = curr_time; +} + +void do_gc(GC_Session *c, void *userdata) +{ + if (c == nullptr) { + return; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + const GC_Conn_State state = chat->connection_state; + + if (state == CS_NONE) { + continue; + } + + if (state != CS_DISCONNECTED) { + do_peer_connections(c, chat, userdata); + do_gc_tcp(c, chat, userdata); + do_handshakes(chat); + do_self_connection(c, chat); + } + + if (chat->connection_state == CS_CONNECTED) { + do_gc_ping_and_key_rotation(chat); + do_timed_out_reconn(chat); + } + + do_new_connection_cooldown(chat); + do_peer_delete(c, chat, userdata); + } +} + +/** @brief Set the size of the groupchat list to n. + * + * Return true on success. + */ +non_null() +static bool realloc_groupchats(GC_Session *c, uint32_t n) +{ + if (n == 0) { + free(c->chats); + c->chats = nullptr; + return true; + } + + GC_Chat *temp = (GC_Chat *)realloc(c->chats, n * sizeof(GC_Chat)); + + if (temp == nullptr) { + return false; + } + + c->chats = temp; + return true; +} + +non_null() +static int get_new_group_index(GC_Session *c) +{ + if (c == nullptr) { + return -1; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + if (c->chats[i].connection_state == CS_NONE) { + return i; + } + } + + if (!realloc_groupchats(c, c->chats_index + 1)) { + return -1; + } + + const int new_index = c->chats_index; + + c->chats[new_index] = empty_gc_chat; + + memset(&c->chats[new_index].saved_invites, -1, sizeof(c->chats[new_index].saved_invites)); + + ++c->chats_index; + + return new_index; +} + +/** Attempts to associate new TCP relays with our group connection. */ +static void add_tcp_relays_to_chat(const GC_Session *c, GC_Chat *chat) +{ + const Messenger *m = c->messenger; + + const uint32_t num_relays = tcp_connections_count(nc_get_tcp_c(m->net_crypto)); + + if (num_relays == 0) { + return; + } + + Node_format *tcp_relays = (Node_format *)calloc(num_relays, sizeof(Node_format)); + + if (tcp_relays == nullptr) { + return; + } + + const uint32_t num_copied = tcp_copy_connected_relays(nc_get_tcp_c(m->net_crypto), tcp_relays, (uint16_t)num_relays); + + for (uint32_t i = 0; i < num_copied; ++i) { + add_tcp_relay_global(chat->tcp_conn, &tcp_relays[i].ip_port, tcp_relays[i].public_key); + } + + free(tcp_relays); +} + +non_null() +static bool init_gc_tcp_connection(const GC_Session *c, GC_Chat *chat) +{ + const Messenger *m = c->messenger; + + chat->tcp_conn = new_tcp_connections(chat->log, chat->rng, m->ns, chat->mono_time, chat->self_secret_key, + &m->options.proxy_info); + + if (chat->tcp_conn == nullptr) { + return false; + } + + add_tcp_relays_to_chat(c, chat); + + set_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_packet, c->messenger); + set_oob_packet_tcp_connection_callback(chat->tcp_conn, &handle_gc_tcp_oob_packet, c->messenger); + + return true; +} + +/** Initializes default shared state values. */ +non_null() +static void init_gc_shared_state(GC_Chat *chat, const Group_Privacy_State privacy_state) +{ + chat->shared_state.maxpeers = MAX_GC_PEERS_DEFAULT; + chat->shared_state.privacy_state = privacy_state; + chat->shared_state.topic_lock = GC_TOPIC_LOCK_ENABLED; + chat->shared_state.voice_state = GV_ALL; +} + +/** @brief Initializes the group shared state for the founder. + * + * Return true on success. + */ +non_null() +static bool init_gc_shared_state_founder(GC_Chat *chat, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t name_length) +{ + memcpy(chat->shared_state.founder_public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + memcpy(chat->shared_state.group_name, group_name, name_length); + chat->shared_state.group_name_len = name_length; + chat->shared_state.privacy_state = privacy_state; + + return sign_gc_shared_state(chat); +} + +/** @brief Initializes shared state for moderation object. + * + * This must be called before any moderation + * or sanctions related operations. + */ +non_null() +static void init_gc_moderation(GC_Chat *chat) +{ + memcpy(chat->moderation.founder_public_sig_key, + get_sig_pk(chat->shared_state.founder_public_key), SIG_PUBLIC_KEY_SIZE); + memcpy(chat->moderation.self_public_sig_key, get_sig_pk(chat->self_public_key), SIG_PUBLIC_KEY_SIZE); + memcpy(chat->moderation.self_secret_sig_key, get_sig_pk(chat->self_secret_key), SIG_SECRET_KEY_SIZE); + chat->moderation.shared_state_version = chat->shared_state.version; + chat->moderation.log = chat->log; +} + +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat); + +non_null() +static int create_new_group(GC_Session *c, const uint8_t *nick, size_t nick_length, bool founder, + const Group_Privacy_State privacy_state) +{ + if (nick == nullptr || nick_length == 0) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -1; + } + + const int group_number = get_new_group_index(c); + + if (group_number == -1) { + return -1; + } + + Messenger *m = c->messenger; + GC_Chat *chat = &c->chats[group_number]; + + chat->log = m->log; + chat->rng = m->rng; + + const uint64_t tm = mono_time_get(m->mono_time); + + chat->group_number = group_number; + chat->numpeers = 0; + chat->connection_state = CS_CONNECTING; + chat->net = m->net; + chat->mono_time = m->mono_time; + chat->last_ping_interval = tm; + chat->friend_connection_id = -1; + + if (!create_new_chat_ext_keypair(chat)) { + LOGGER_ERROR(chat->log, "Failed to create extended keypair"); + group_delete(c, chat); + return -1; + } + + if (!init_gc_tcp_connection(c, chat)) { + group_delete(c, chat); + return -1; + } + + if (peer_add(chat, nullptr, chat->self_public_key) != 0) { /* you are always peer_number/index 0 */ + group_delete(c, chat); + return -1; + } + + if (!self_gc_set_nick(chat, nick, (uint16_t)nick_length)) { + group_delete(c, chat); + return -1; + } + + self_gc_set_status(chat, GS_NONE); + self_gc_set_role(chat, founder ? GR_FOUNDER : GR_USER); + self_gc_set_confirmed(chat, true); + self_gc_set_ext_public_key(chat, chat->self_public_key); + + init_gc_shared_state(chat, privacy_state); + init_gc_moderation(chat); + + return group_number; +} + +/** @brief Inits the sanctions list credentials. + * + * This should be called by the group founder on creation. + * + * This function must be called after `init_gc_moderation()`. + * + * Return true on success. + */ +non_null() +static bool init_gc_sanctions_creds(GC_Chat *chat) +{ + return sanctions_list_make_creds(&chat->moderation); +} + +/** @brief Attempts to add `num_addrs` peers from `addrs` to our peerlist and initiate invite requests + * for all of them. + * + * Returns the number of peers successfully loaded. + */ +static size_t load_gc_peers(GC_Chat *chat, const GC_SavedPeerInfo *addrs, uint16_t num_addrs) +{ + size_t count = 0; + + for (size_t i = 0; i < num_addrs; ++i) { + if (!saved_peer_is_valid(&addrs[i])) { + continue; + } + + const bool ip_port_is_set = ipport_isset(&addrs[i].ip_port); + const IP_Port *ip_port = ip_port_is_set ? &addrs[i].ip_port : nullptr; + + const int peer_number = peer_add(chat, ip_port, addrs[i].public_key); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + continue; + } + + add_tcp_relay_global(chat->tcp_conn, &addrs[i].tcp_relay.ip_port, addrs[i].tcp_relay.public_key); + + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &addrs[i].tcp_relay.ip_port, + addrs[i].tcp_relay.public_key); + + if (add_tcp_result == -1 && !ip_port_is_set) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + continue; + } + + if (add_tcp_result == 0) { + const int save_tcp_result = gcc_save_tcp_relay(chat->rng, gconn, &addrs[i].tcp_relay); + + if (save_tcp_result == -1) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_DISCONNECTED, nullptr, 0); + continue; + } + + memcpy(gconn->oob_relay_pk, addrs[i].tcp_relay.public_key, CRYPTO_PUBLIC_KEY_SIZE); + } + + const uint64_t tm = mono_time_get(chat->mono_time); + + gconn->is_oob_handshake = !gcc_direct_conn_is_possible(chat, gconn); + gconn->is_pending_handshake_response = false; + gconn->pending_handshake_type = HS_INVITE_REQUEST; + gconn->last_received_packet_time = tm; + gconn->last_key_rotation = tm; + + ++count; + } + + update_gc_peer_roles(chat); + + return count; +} + +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp) +{ + gc_save_pack_group(chat, bp); +} + +int gc_group_load(GC_Session *c, Bin_Unpack *bu) +{ + const int group_number = get_new_group_index(c); + + if (group_number < 0) { + return -1; + } + + const uint64_t tm = mono_time_get(c->messenger->mono_time); + + Messenger *m = c->messenger; + GC_Chat *chat = &c->chats[group_number]; + + chat->group_number = group_number; + chat->numpeers = 0; + chat->net = m->net; + chat->mono_time = m->mono_time; + chat->log = m->log; + chat->rng = m->rng; + chat->last_ping_interval = tm; + chat->friend_connection_id = -1; + + if (!gc_load_unpack_group(chat, bu)) { + LOGGER_ERROR(chat->log, "Failed to unpack group"); + return -1; + } + + init_gc_moderation(chat); + + if (!init_gc_tcp_connection(c, chat)) { + LOGGER_ERROR(chat->log, "Failed to init tcp connection"); + return -1; + } + + if (chat->connection_state == CS_DISCONNECTED) { + return group_number; + } + + if (is_public_chat(chat)) { + if (!m_create_group_connection(m, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + } + } + + return group_number; +} + +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t group_name_length, + const uint8_t *nick, size_t nick_length) +{ + if (group_name_length > MAX_GC_GROUP_NAME_SIZE) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -1; + } + + if (group_name_length == 0 || group_name == nullptr) { + return -2; + } + + if (nick_length == 0 || nick == nullptr) { + return -2; + } + + const int group_number = create_new_group(c, nick, nick_length, true, privacy_state); + + if (group_number == -1) { + return -3; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -3; + } + + crypto_memlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); + + create_extended_keypair(chat->chat_public_key, chat->chat_secret_key); + + if (!init_gc_shared_state_founder(chat, privacy_state, group_name, group_name_length)) { + group_delete(c, chat); + return -4; + } + + init_gc_moderation(chat); + + if (!init_gc_sanctions_creds(chat)) { + group_delete(c, chat); + return -4; + } + + if (gc_set_topic(chat, nullptr, 0) != 0) { + group_delete(c, chat); + return -4; + } + + chat->join_type = HJ_PRIVATE; + chat->connection_state = CS_CONNECTED; + chat->time_connected = mono_time_get(c->messenger->mono_time); + + if (is_public_chat(chat)) { + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_ERROR(chat->log, "Failed to initialize group friend connection"); + group_delete(c, chat); + return -5; + } + + chat->join_type = HJ_PUBLIC; + } + + update_gc_peer_roles(chat); + + return group_number; +} + +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, + uint16_t passwd_len) +{ + if (chat_id == nullptr || group_exists(c, chat_id) || getfriend_id(c->messenger, chat_id) != -1) { + return -2; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -3; + } + + if (nick == nullptr || nick_length == 0) { + return -4; + } + + const int group_number = create_new_group(c, nick, nick_length, false, GI_PUBLIC); + + if (group_number == -1) { + return -1; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -1; + } + + if (!expand_chat_id(chat->chat_public_key, chat_id)) { + group_delete(c, chat); + return -1; + } + + chat->connection_state = CS_CONNECTING; + + if (passwd != nullptr && passwd_len > 0) { + if (!set_gc_password_local(chat, passwd, passwd_len)) { + group_delete(c, chat); + return -5; + } + } + + if (!m_create_group_connection(c->messenger, chat)) { + group_delete(c, chat); + return -6; + } + + update_gc_peer_roles(chat); + + return group_number; +} + +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + return false; + } + + chat->connection_state = CS_DISCONNECTED; + + send_gc_broadcast_message(chat, nullptr, 0, GM_PEER_EXIT); + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + return true; +} + +int gc_rejoin_group(GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + return -1; + } + + chat->time_connected = 0; + + if (group_can_handle_packets(chat)) { + send_gc_self_exit(chat, nullptr, 0); + } + + for (uint32_t i = 1; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_SELF_DISCONNECTED, nullptr, 0); + } + + if (is_public_chat(chat)) { + kill_group_friend_connection(c, chat); + + if (!m_create_group_connection(c->messenger, chat)) { + LOGGER_WARNING(chat->log, "Failed to create new messenger connection for group"); + return -2; + } + + chat->update_self_announces = true; + } + + chat->connection_state = CS_CONNECTING; + + return 0; +} + +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length) +{ + if (length < CHAT_ID_SIZE) { + return false; + } + + return !group_exists(c, chat_id); +} + +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, + gc_send_group_invite_packet_cb *callback) +{ + if (!friend_is_valid(c->messenger, friend_number)) { + return -1; + } + + const uint16_t group_name_length = chat->shared_state.group_name_len; + + assert(group_name_length <= MAX_GC_GROUP_NAME_SIZE); + + uint8_t *packet = (uint8_t *)malloc(2 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE + group_name_length); + + if (packet == nullptr) { + return -1; + } + + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE; + + memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + uint16_t length = 2 + CHAT_ID_SIZE; + + memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + + + memcpy(packet + length, chat->shared_state.group_name, group_name_length); + length += group_name_length; + + assert(length <= MAX_GC_PACKET_SIZE); + + if (!callback(c->messenger, friend_number, packet, length)) { + free(packet); + return -2; + } + + free(packet); + + chat->saved_invites[chat->saved_invites_index] = friend_number; + chat->saved_invites_index = (chat->saved_invites_index + 1) % MAX_GC_SAVED_INVITES; + + return 0; +} + +/** @brief Sends an invite accepted packet to `friend_number`. + * + * Return 0 on success. + * Return -1 if `friend_number` does not designate a valid friend. + * Return -2 if `chat `is null. + * Return -3 if packet failed to send. + */ +non_null() +static int send_gc_invite_accepted_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number) +{ + if (!friend_is_valid(m, friend_number)) { + return -1; + } + + if (chat == nullptr) { + return -2; + } + + uint8_t packet[1 + 1 + CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE]; + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE_ACCEPTED; + + memcpy(packet + 2, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE); + uint16_t length = 2 + CHAT_ID_SIZE; + + memcpy(packet + length, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + length += ENC_PUBLIC_KEY_SIZE; + + if (!send_group_invite_packet(m, friend_number, packet, length)) { + LOGGER_ERROR(chat->log, "Failed to send group invite packet."); + return -3; + } + + return 0; +} + +/** @brief Sends an invite confirmed packet to friend designated by `friend_number`. + * + * `data` must contain the group's Chat ID, the sender's public encryption key, + * and either the sender's packed IP_Port, or at least one packed TCP node that + * the sender can be connected to through (or both). + * + * Return true on success. + */ +non_null() +static bool send_gc_invite_confirmed_packet(const Messenger *m, const GC_Chat *chat, uint32_t friend_number, + const uint8_t *data, uint16_t length) +{ + if (!friend_is_valid(m, friend_number)) { + return false; + } + + if (chat == nullptr) { + return false; + } + + if (length > MAX_GC_PACKET_SIZE) { + return false; + } + + const uint16_t packet_length = 2 + length; + uint8_t *packet = (uint8_t *)malloc(packet_length); + + if (packet == nullptr) { + return false; + } + + packet[0] = GP_FRIEND_INVITE; + packet[1] = GROUP_INVITE_CONFIRMATION; + + memcpy(packet + 2, data, length); + + if (!send_group_invite_packet(m, friend_number, packet, packet_length)) { + free(packet); + return false; + } + + free(packet); + + return true; +} + +/** @brief Adds `num_nodes` tcp relays from `tcp_relays` to tcp relays list associated with `gconn` + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays(const GC_Chat *chat, GC_Connection *gconn, const Node_format *tcp_relays, + size_t num_nodes) +{ + uint32_t relays_added = 0; + + for (size_t i = 0; i < num_nodes; ++i) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, + gconn->tcp_connection_num, &tcp_relays[i].ip_port, + tcp_relays[i].public_key); + + if (add_tcp_result == 0) { + if (gcc_save_tcp_relay(chat->rng, gconn, &tcp_relays[i]) == 0) { + ++relays_added; + } + } + } + + return relays_added; +} + +non_null() +static bool copy_friend_ip_port_to_gconn(const Messenger *m, int friend_number, GC_Connection *gconn) +{ + if (!friend_is_valid(m, friend_number)) { + return false; + } + + const Friend *f = &m->friendlist[friend_number]; + const int friend_connection_id = f->friendcon_id; + const Friend_Conn *connection = get_conn(m->fr_c, friend_connection_id); + + if (connection == nullptr) { + return false; + } + + const IP_Port *friend_ip_port = friend_conn_get_dht_ip_port(connection); + + if (!ipport_isset(friend_ip_port)) { + return false; + } + + gconn->addr.ip_port = *friend_ip_port; + + return true; +} + +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ + if (length < GC_JOIN_DATA_LENGTH) { + return -1; + } + + if (!friend_is_valid(c->messenger, friend_number)) { + return -4; + } + + uint8_t chat_id[CHAT_ID_SIZE]; + uint8_t invite_chat_pk[ENC_PUBLIC_KEY_SIZE]; + + memcpy(chat_id, data, CHAT_ID_SIZE); + memcpy(invite_chat_pk, data + CHAT_ID_SIZE, ENC_PUBLIC_KEY_SIZE); + + const GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + + if (chat == nullptr) { + return -2; + } + + const int peer_number = get_peer_number_of_enc_pk(chat, invite_chat_pk, false); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return -3; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const int num_nodes = unpack_nodes(tcp_relays, GCC_MAX_TCP_SHARED_RELAYS, + nullptr, data + ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE, + length - GC_JOIN_DATA_LENGTH, true); + + const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(c->messenger, friend_number, gconn); + + uint32_t tcp_relays_added = 0; + + if (num_nodes > 0) { + tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_nodes); + } + + if (tcp_relays_added == 0 && !copy_ip_port_result) { + LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); + return -5; + } + + gconn->pending_handshake_type = HS_INVITE_REQUEST; + + return 0; +} + +/** Return true if we have a pending sent invite for our friend designated by `friend_number`. */ +non_null() +static bool friend_was_invited(const Messenger *m, GC_Chat *chat, int friend_number) +{ + for (size_t i = 0; i < MAX_GC_SAVED_INVITES; ++i) { + if (chat->saved_invites[i] == friend_number) { + chat->saved_invites[i] = -1; + return friend_is_valid(m, friend_number); + } + } + + return false; +} + +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length) +{ + if (length < GC_JOIN_DATA_LENGTH) { + return false; + } + + const Messenger *m = c->messenger; + + const uint8_t *chat_id = data; + + GC_Chat *chat = gc_get_group_by_public_key(c, chat_id); + + if (chat == nullptr || !group_can_handle_packets(chat)) { + return false; + } + + const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + + const int peer_number = peer_add(chat, nullptr, invite_chat_pk); + + if (!friend_was_invited(m, chat, friend_number)) { + return false; + } + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return false; + } + + Node_format tcp_relays[GCC_MAX_TCP_SHARED_RELAYS]; + const uint32_t num_tcp_relays = tcp_copy_connected_relays(chat->tcp_conn, tcp_relays, GCC_MAX_TCP_SHARED_RELAYS); + + const bool copy_ip_port_result = copy_friend_ip_port_to_gconn(m, friend_number, gconn); + + if (num_tcp_relays == 0 && !copy_ip_port_result) { + return false; + } + + uint16_t len = GC_JOIN_DATA_LENGTH; + uint8_t out_data[GC_JOIN_DATA_LENGTH + (GCC_MAX_TCP_SHARED_RELAYS * PACKED_NODE_SIZE_IP6)]; + + memcpy(out_data, chat_id, CHAT_ID_SIZE); + memcpy(out_data + CHAT_ID_SIZE, chat->self_public_key, ENC_PUBLIC_KEY_SIZE); + + if (num_tcp_relays > 0) { + const uint32_t tcp_relays_added = add_gc_tcp_relays(chat, gconn, tcp_relays, num_tcp_relays); + + if (tcp_relays_added == 0 && !copy_ip_port_result) { + LOGGER_ERROR(chat->log, "Got invalid connection info from peer"); + return false; + } + + const int nodes_len = pack_nodes(chat->log, out_data + len, sizeof(out_data) - len, tcp_relays, + (uint16_t)num_tcp_relays); + + if (nodes_len <= 0 && !copy_ip_port_result) { + return false; + } + + len += nodes_len; + } + + return send_gc_invite_confirmed_packet(m, chat, friend_number, out_data, len); +} + +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, + size_t nick_length, const uint8_t *passwd, uint16_t passwd_len) +{ + if (length < CHAT_ID_SIZE + ENC_PUBLIC_KEY_SIZE) { + return -1; + } + + if (nick_length > MAX_GC_NICK_SIZE) { + return -3; + } + + if (nick == nullptr || nick_length == 0) { + return -4; + } + + if (!friend_is_valid(c->messenger, friend_number)) { + return -6; + } + + const uint8_t *chat_id = data; + const uint8_t *invite_chat_pk = data + CHAT_ID_SIZE; + + const int group_number = create_new_group(c, nick, nick_length, false, GI_PRIVATE); + + if (group_number == -1) { + return -2; + } + + GC_Chat *chat = gc_get_group(c, group_number); + + if (chat == nullptr) { + return -2; + } + + if (!expand_chat_id(chat->chat_public_key, chat_id)) { + group_delete(c, chat); + return -2; + } + + if (passwd != nullptr && passwd_len > 0) { + if (!set_gc_password_local(chat, passwd, passwd_len)) { + group_delete(c, chat); + return -5; + } + } + + const int peer_id = peer_add(chat, nullptr, invite_chat_pk); + + if (peer_id < 0) { + return -2; + } + + chat->join_type = HJ_PRIVATE; + + if (send_gc_invite_accepted_packet(c->messenger, chat, friend_number) != 0) { + return -7; + } + + return group_number; +} + +non_null(1, 3) nullable(5) +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data); + +GC_Session *new_dht_groupchats(Messenger *m) +{ + if (m == nullptr) { + return nullptr; + } + + GC_Session *c = (GC_Session *)calloc(1, sizeof(GC_Session)); + + if (c == nullptr) { + return nullptr; + } + + c->messenger = m; + c->announces_list = m->group_announce; + + networking_registerhandler(m->net, NET_PACKET_GC_LOSSLESS, &handle_gc_udp_packet, m); + networking_registerhandler(m->net, NET_PACKET_GC_LOSSY, &handle_gc_udp_packet, m); + networking_registerhandler(m->net, NET_PACKET_GC_HANDSHAKE, &handle_gc_udp_packet, m); + onion_group_announce_register(m->onion_c, gc_handle_announce_response_callback, c); + + return c; +} + +static void group_cleanup(GC_Session *c, GC_Chat *chat) +{ + kill_group_friend_connection(c, chat); + + mod_list_cleanup(&chat->moderation); + sanctions_list_cleanup(&chat->moderation); + + if (chat->tcp_conn != nullptr) { + kill_tcp_connections(chat->tcp_conn); + } + + gcc_cleanup(chat); + + if (chat->group != nullptr) { + free(chat->group); + chat->group = nullptr; + } + + crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + crypto_memunlock(chat->chat_secret_key, sizeof(chat->chat_secret_key)); + crypto_memunlock(chat->shared_state.password, sizeof(chat->shared_state.password)); +} + +/** Deletes chat from group chat array and cleans up. */ +static void group_delete(GC_Session *c, GC_Chat *chat) +{ + if (c == nullptr || chat == nullptr) { + if (chat != nullptr) { + LOGGER_ERROR(chat->log, "Null pointer"); + } + + return; + } + + group_cleanup(c, chat); + + c->chats[chat->group_number] = empty_gc_chat; + + uint32_t i; + + for (i = c->chats_index; i > 0; --i) { + if (c->chats[i - 1].connection_state != CS_NONE) { + break; + } + } + + if (c->chats_index != i) { + c->chats_index = i; + + if (!realloc_groupchats(c, c->chats_index)) { + LOGGER_ERROR(chat->log, "Failed to reallocate groupchats array"); + } + } +} + +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length) +{ + const int ret = group_can_handle_packets(chat) ? send_gc_self_exit(chat, message, length) : 0; + group_delete(c, chat); + return ret; +} + +void kill_dht_groupchats(GC_Session *c) +{ + if (c == nullptr) { + return; + } + + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (group_can_handle_packets(chat)) { + send_gc_self_exit(chat, nullptr, 0); + } + + group_cleanup(c, chat); + } + + networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSY, nullptr, nullptr); + networking_registerhandler(c->messenger->net, NET_PACKET_GC_LOSSLESS, nullptr, nullptr); + networking_registerhandler(c->messenger->net, NET_PACKET_GC_HANDSHAKE, nullptr, nullptr); + onion_group_announce_register(c->messenger->onion_c, nullptr, nullptr); + + free(c->chats); + free(c); +} + +bool gc_group_is_valid(const GC_Chat *chat) +{ + return chat->connection_state != CS_NONE && chat->shared_state.version > 0; +} + +/** Return true if `group_number` designates an active group in session `c`. */ +static bool group_number_valid(const GC_Session *c, int group_number) +{ + if (group_number < 0 || group_number >= c->chats_index) { + return false; + } + + if (c->chats == nullptr) { + return false; + } + + return c->chats[group_number].connection_state != CS_NONE; +} + +uint32_t gc_count_groups(const GC_Session *c) +{ + uint32_t count = 0; + + for (uint32_t i = 0; i < c->chats_index; ++i) { + const GC_Chat *chat = &c->chats[i]; + + if (gc_group_is_valid(chat)) { + ++count; + } + } + + return count; +} + +GC_Chat *gc_get_group(const GC_Session *c, int group_number) +{ + if (!group_number_valid(c, group_number)) { + return nullptr; + } + + return &c->chats[group_number]; +} + +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key) +{ + for (uint32_t i = 0; i < c->chats_index; ++i) { + GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(public_key, get_chat_id(chat->chat_public_key), CHAT_ID_SIZE) == 0) { + return chat; + } + } + + return nullptr; +} + +/** Return True if chat_id exists in the session chat array */ +static bool group_exists(const GC_Session *c, const uint8_t *chat_id) +{ + for (uint32_t i = 0; i < c->chats_index; ++i) { + const GC_Chat *chat = &c->chats[i]; + + if (chat->connection_state == CS_NONE) { + continue; + } + + if (memcmp(get_chat_id(chat->chat_public_key), chat_id, CHAT_ID_SIZE) == 0) { + return true; + } + } + + return false; +} + +/** Creates a new 32-byte session encryption keypair and puts the results in `public_key` and `secret_key`. */ +static void create_gc_session_keypair(const Logger *log, const Random *rng, uint8_t *public_key, uint8_t *secret_key) +{ + if (crypto_new_keypair(rng, public_key, secret_key) != 0) { + LOGGER_FATAL(log, "Failed to create group session keypair"); + } +} + +/** + * Creates a new 64-byte extended keypair for `chat` and puts results in `self_public_key` + * and `self_secret_key` buffers. The first 32-bytes of the generated keys are used for + * encryption, while the remaining 32-bytes are used for signing. + * + * Return false if key generation fails. + */ +non_null() +static bool create_new_chat_ext_keypair(GC_Chat *chat) +{ + crypto_memlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + + if (!create_extended_keypair(chat->self_public_key, chat->self_secret_key)) { + crypto_memunlock(chat->self_secret_key, sizeof(chat->self_secret_key)); + return false; + } + + return true; +} + +/** @brief Handles a group announce onion response. + * + * Return true on success. + */ +static bool gc_handle_announce_response_callback(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data) +{ + const GC_Session *c = (GC_Session *)user_data; + + if (c == nullptr) { + return false; + } + + if (sendback_num == 0) { + return false; + } + + GC_Announce announces[GCA_MAX_SENT_ANNOUNCES]; + const uint8_t *gc_public_key = onion_friend_get_gc_public_key_num(onion_c, sendback_num - 1); + GC_Chat *chat = gc_get_group_by_public_key(c, gc_public_key); + + if (chat == nullptr) { + return false; + } + + const int gc_announces_count = gca_unpack_announces_list(chat->log, data, data_length, + announces, GCA_MAX_SENT_ANNOUNCES); + + if (gc_announces_count == -1) { + return false; + } + + const int added_peers = gc_add_peers_from_announces(chat, announces, gc_announces_count); + + return added_peers >= 0; +} + +/** @brief Adds TCP relays from `announce` to the TCP relays list for `gconn`. + * + * Returns the number of relays successfully added. + */ +non_null() +static uint32_t add_gc_tcp_relays_from_announce(const GC_Chat *chat, GC_Connection *gconn, const GC_Announce *announce) +{ + uint32_t added_relays = 0; + + for (uint8_t j = 0; j < announce->tcp_relays_count; ++j) { + const int add_tcp_result = add_tcp_relay_connection(chat->tcp_conn, gconn->tcp_connection_num, + &announce->tcp_relays[j].ip_port, + announce->tcp_relays[j].public_key); + + if (add_tcp_result == -1) { + continue; + } + + if (gcc_save_tcp_relay(chat->rng, gconn, &announce->tcp_relays[j]) == -1) { + continue; + } + + if (added_relays == 0) { + memcpy(gconn->oob_relay_pk, announce->tcp_relays[j].public_key, CRYPTO_PUBLIC_KEY_SIZE); + } + + ++added_relays; + } + + return added_relays; +} + +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count) +{ + if (chat == nullptr || announces == nullptr) { + return -1; + } + + if (!is_public_chat(chat)) { + return 0; + } + + int added_peers = 0; + + for (uint8_t i = 0; i < gc_announces_count; ++i) { + const GC_Announce *announce = &announces[i]; + + if (!gca_is_valid_announce(announce)) { + continue; + } + + const bool ip_port_set = announce->ip_port_is_set; + const IP_Port *ip_port = ip_port_set ? &announce->ip_port : nullptr; + const int peer_number = peer_add(chat, ip_port, announce->peer_public_key); + + GC_Connection *gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + continue; + } + + const uint32_t added_tcp_relays = add_gc_tcp_relays_from_announce(chat, gconn, announce); + + if (!ip_port_set && added_tcp_relays == 0) { + LOGGER_ERROR(chat->log, "Got invalid announcement: %u relays, IPP set: %d", + added_tcp_relays, ip_port_set); + continue; + } + + gconn->pending_handshake_type = HS_INVITE_REQUEST; + + if (!ip_port_set) { + gconn->is_oob_handshake = true; + } + + ++added_peers; + } + + return added_peers; +} +#endif // VANILLA_NACL diff --git a/toxcore/group_chats.h b/toxcore/group_chats.h new file mode 100644 index 0000000000..8924023657 --- /dev/null +++ b/toxcore/group_chats.h @@ -0,0 +1,783 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CHATS_H +#define GROUP_CHATS_H + +#include +#include + +#include "TCP_connection.h" +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_announce.h" +#include "group_common.h" +#include "group_connection.h" +#include "logger.h" + +#define GC_PING_TIMEOUT 12 +#define GC_SEND_IP_PORT_INTERVAL (GC_PING_TIMEOUT * 5) +#define GC_CONFIRMED_PEER_TIMEOUT (GC_PING_TIMEOUT * 4 + 10) +#define GC_UNCONFIRMED_PEER_TIMEOUT GC_PING_TIMEOUT + +#define GC_JOIN_DATA_LENGTH (ENC_PUBLIC_KEY_SIZE + CHAT_ID_SIZE) + +/** Group topic lock states. */ +typedef enum Group_Topic_Lock { + TL_ENABLED = 0x00, // Only the Founder and moderators may set the topic + TL_DISABLED = 0x01, // Anyone except Observers may set the topic +} Group_Topic_Lock; + +/** Group moderation events. */ +typedef enum Group_Moderation_Event { + MV_KICK = 0x00, // A peer has been kicked + MV_OBSERVER = 0x01, // A peer has been demoted to Observer + MV_USER = 0x02, // A peer has been demoted or promoted to User + MV_MOD = 0x03, // A peer has been promoted to or demoted from Moderator +} Group_Moderation_Event; + +/** Messenger level group invite types */ +typedef enum Group_Invite_Message_Type { + GROUP_INVITE = 0x00, // Peer has initiated an invite + GROUP_INVITE_ACCEPTED = 0x01, // Peer has accepted the invite + GROUP_INVITE_CONFIRMATION = 0x02, // Peer has confirmed the accepted invite +} Group_Invite_Message_Type; + +/** Group join rejection types. */ +typedef enum Group_Join_Rejected { + GJ_GROUP_FULL = 0x00, + GJ_INVALID_PASSWORD = 0x01, + GJ_INVITE_FAILED = 0x02, + GJ_INVALID = 0x03, +} Group_Join_Rejected; + +/** Group broadcast packet types */ +typedef enum Group_Broadcast_Type { + GM_STATUS = 0x00, // Peer changed their status + GM_NICK = 0x01, // Peer changed their nickname + GM_PLAIN_MESSAGE = 0x02, // Peer sent a normal message + GM_ACTION_MESSAGE = 0x03, // Peer sent an action message + GM_PRIVATE_MESSAGE = 0x04, // Peer sent a private message + GM_PEER_EXIT = 0x05, // Peer left the group + GM_KICK_PEER = 0x06, // Peer was kicked from the group + GM_SET_MOD = 0x07, // Peer was promoted to or demoted from Moderator role + GM_SET_OBSERVER = 0x08, // Peer was demoted to or promoted from Observer role +} Group_Broadcast_Type; + +/*** + * Group packet types. + * + * For a detailed spec, see docs/DHT_Group_Chats_Packet_Spec.md + */ +typedef enum Group_Packet_Type { + /* lossy packets (ID 0 is reserved) */ + GP_PING = 0x01, + GP_MESSAGE_ACK = 0x02, + GP_INVITE_RESPONSE_REJECT = 0x03, + + /* lossless packets */ + GP_CUSTOM_PRIVATE_PACKET = 0xee, + GP_FRAGMENT = 0xef, + GP_KEY_ROTATION = 0xf0, + GP_TCP_RELAYS = 0xf1, + GP_CUSTOM_PACKET = 0xf2, + GP_BROADCAST = 0xf3, + GP_PEER_INFO_REQUEST = 0xf4, + GP_PEER_INFO_RESPONSE = 0xf5, + GP_INVITE_REQUEST = 0xf6, + GP_INVITE_RESPONSE = 0xf7, + GP_SYNC_REQUEST = 0xf8, + GP_SYNC_RESPONSE = 0xf9, + GP_TOPIC = 0xfa, + GP_SHARED_STATE = 0xfb, + GP_MOD_LIST = 0xfc, + GP_SANCTIONS_LIST = 0xfd, + GP_FRIEND_INVITE = 0xfe, + GP_HS_RESPONSE_ACK = 0xff, +} Group_Packet_Type; + +/** Lossless message acknowledgement types. */ +typedef enum Group_Message_Ack_Type { + GR_ACK_RECV = 0x00, // indicates a message has been received + GR_ACK_REQ = 0x01, // indicates a message needs to be re-sent +} Group_Message_Ack_Type; + +/** @brief Returns the GC_Connection object associated with `peer_number`. + * Returns null if peer_number does not designate a valid peer. + */ +non_null() +GC_Connection *get_gc_connection(const GC_Chat *chat, int peer_number); + +/** @brief Returns the jenkins hash of a 32 byte public encryption key. */ +non_null() +uint32_t gc_get_pk_jenkins_hash(const uint8_t *public_key); + +/** @brief Check if peer with the public encryption key is in peer list. + * + * Returns the peer number if peer is in the peer list. + * Returns -1 if peer is not in the peer list. + * + * If `confirmed` is true the peer number will only be returned if the peer is confirmed. + */ +non_null() +int get_peer_number_of_enc_pk(const GC_Chat *chat, const uint8_t *public_enc_key, bool confirmed); + +/** @brief Encrypts `data` of size `length` using the peer's shared key and a new nonce. + * + * Adds encrypted header consisting of: packet type, message_id (only for lossless packets). + * Adds plaintext header consisting of: packet identifier, self public encryption key, nonce. + * + * Return length of encrypted packet on success. + * Return -1 if plaintext length is invalid. + * Return -2 if malloc fails. + * Return -3 if encryption fails. + */ +non_null(1, 2, 3, 4, 5) nullable(7) +int group_packet_wrap( + const Logger *log, const Random *rng, const uint8_t *self_pk, const uint8_t *shared_key, uint8_t *packet, + uint16_t packet_size, const uint8_t *data, uint16_t length, uint64_t message_id, + uint8_t gp_packet_type, uint8_t net_packet_type); + +/** @brief Returns the size of a wrapped/encrypted packet with a plain size of `length`. + * + * `packet_type` should be either NET_PACKET_GC_LOSSY or NET_PACKET_GC_LOSSLESS. + */ +uint16_t gc_get_wrapped_packet_size(uint16_t length, Net_Packet_Type packet_type); + +/** @brief Sends a plain message or an action, depending on type. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * `message_id` should either point to a uint32_t or be NULL. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the message type is invalid. + * Returns -4 if the sender does not have permission to speak. + * Returns -5 if the packet fails to send. + */ +non_null(1, 2, 3, 4) nullable(5) +int gc_send_message(const GC_Chat *chat, const uint8_t *message, uint16_t length, uint8_t type, + uint32_t *message_id); + +/** @brief Sends a private message to peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the peer_id is invalid. + * Returns -4 if the message type is invalid. + * Returns -5 if the sender has the observer role. + * Returns -6 if the packet fails to send. + */ +non_null() +int gc_send_private_message(const GC_Chat *chat, uint32_t peer_id, uint8_t type, const uint8_t *message, + uint16_t length); + +/** @brief Sends a custom packet to the group. If lossless is true, the packet will be lossless. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * Returns 0 on success. + * Returns -1 if the message is too long. + * Returns -2 if the message pointer is NULL or length is zero. + * Returns -3 if the sender has the observer role. + */ +non_null() +int gc_send_custom_packet(const GC_Chat *chat, bool lossless, const uint8_t *data, uint16_t length); + +/** @brief Sends a custom private packet to the peer designated by peer_id. + * + * `length` must not exceed MAX_GC_MESSAGE_SIZE and must not be equal to zero. + * + * @retval 0 on success. + * @retval -1 if the message is too long. + * @retval -2 if the message pointer is NULL or length is zero. + * @retval -3 if the supplied peer_id does not designate a valid peer. + * @retval -4 if the sender has the observer role. + * @retval -5 if the packet fails to send. + */ +non_null() +int gc_send_custom_private_packet(const GC_Chat *chat, bool lossless, uint32_t peer_id, const uint8_t *message, + uint16_t length); + +/** @brief Sets ignore for peer_id. + * + * Returns 0 on success. + * Returns -1 if the peer_id is invalid. + * Returns -2 if the caller attempted to ignore himself. + */ +non_null() +int gc_set_ignore(const GC_Chat *chat, uint32_t peer_id, bool ignore); + +/** @brief Sets the group topic and broadcasts it to the group. + * + * If `length` is equal to zero the topic will be unset. + * + * Returns 0 on success. + * Returns -1 if the topic is too long (must be `<= MAX_GC_TOPIC_SIZE`). + * Returns -2 if the caller does not have the required permissions to set the topic. + * Returns -3 if the packet cannot be created or signing fails. + * Returns -4 if the packet fails + */ +non_null(1) nullable(2) +int gc_set_topic(GC_Chat *chat, const uint8_t *topic, uint16_t length); + +/** @brief Copies the group topic to `topic`. If topic is null this function has no effect. + * + * Call `gc_get_topic_size` to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last topic callback. + */ +non_null(1) nullable(2) +void gc_get_topic(const GC_Chat *chat, uint8_t *topic); + +/** @brief Returns the topic length. + * + * The return value is equal to the `length` agument received by the last topic + * callback. + */ +non_null() +uint16_t gc_get_topic_size(const GC_Chat *chat); + +/** @brief Copies group name to `group_name`. If `group_name` is null this function has no effect. + * + * Call `gc_get_group_name_size` to determine the allocation size for the `group_name` + * parameter. + */ +non_null() +void gc_get_group_name(const GC_Chat *chat, uint8_t *group_name); + +/** @brief Returns the group name length. */ +non_null() +uint16_t gc_get_group_name_size(const GC_Chat *chat); + +/** @brief Copies the group password to password. + * + * If password is null this function has no effect. + * + * Call the `gc_get_password_size` function to determine the allocation size for + * the `password` buffer. + * + * The data received is equal to the data received by the last password callback. + */ +non_null() +void gc_get_password(const GC_Chat *chat, uint8_t *password); + +/** @brief Returns the group password length. */ +non_null() +uint16_t gc_get_password_size(const GC_Chat *chat); + +/** @brief Returns the group privacy state. + * + * The value returned is equal to the data receieved by the last privacy_state callback. + */ +non_null() +Group_Privacy_State gc_get_privacy_state(const GC_Chat *chat); + +/** @brief Returns the group topic lock state. + * + * The value returned is equal to the data received by the last last topic_lock callback. + */ +non_null() +Group_Topic_Lock gc_get_topic_lock_state(const GC_Chat *chat); + +/** @brief Returns the group voice state. + * + * The value returned is equal to the data received by the last voice_state callback. + */ +non_null() +Group_Voice_State gc_get_voice_state(const GC_Chat *chat); + +/** @brief Returns the group peer limit. + * + * The value returned is equal to the data receieved by the last peer_limit callback. + */ +non_null() +uint16_t gc_get_max_peers(const GC_Chat *chat); + +/** @brief Sets your own nick to `nick`. + * + * `length` cannot exceed MAX_GC_NICK_SIZE. if `length` is zero or `name` is a + * null pointer the function call will fail. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the length is too long. + * Returns -3 if the length is zero or nick is a NULL pointer. + * Returns -4 if the packet fails to send. + */ +non_null() +int gc_set_self_nick(const Messenger *m, int group_number, const uint8_t *nick, uint16_t length); + +/** @brief Copies your own name to `nick`. + * + * If `nick` is null this function has no effect. + */ +non_null() +void gc_get_self_nick(const GC_Chat *chat, uint8_t *nick); + +/** @brief Return your own nick length. + * + * If no nick was set before calling this function it will return 0. + */ +non_null() +uint16_t gc_get_self_nick_size(const GC_Chat *chat); + +/** @brief Returns your own group role. */ +non_null() +Group_Role gc_get_self_role(const GC_Chat *chat); + +/** @brief Return your own status. */ +non_null() +uint8_t gc_get_self_status(const GC_Chat *chat); + +/** @brief Returns your own peer id. */ +non_null() +uint32_t gc_get_self_peer_id(const GC_Chat *chat); + +/** @brief Copies self public key to `public_key`. + * + * If `public_key` is null this function has no effect. + * + * This key is permanently tied to our identity for `chat` until we explicitly + * exit the group. This key is the only way for other peers to reliably identify + * us across client restarts. + */ +non_null(1) nullable(2) +void gc_get_self_public_key(const GC_Chat *chat, uint8_t *public_key); + +/** @brief Copies nick designated by `peer_id` to `name`. + * + * Call `gc_get_peer_nick_size` to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last nick_change callback. + * + * Returns true on success. + * Returns false if peer_id is invalid. + */ +non_null(1) nullable(3) +bool gc_get_peer_nick(const GC_Chat *chat, uint32_t peer_id, uint8_t *name); + +/** @brief Returns the length of the nick for the peer designated by `peer_id`. + * Returns -1 if peer_id is invalid. + * + * The value returned is equal to the `length` argument received by the last + * nick_change callback. + */ +non_null() +int gc_get_peer_nick_size(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Copies peer_id's public key to `public_key`. + * + * This key is permanently tied to the peer's identity for `chat` until they explicitly + * exit the group. This key is the only way for to reliably identify the given peer + * across client restarts. + * + * `public_key` shold have room for at least ENC_PUBLIC_KEY_SIZE bytes. + * + * Returns 0 on success. + * Returns -1 if peer_id is invalid or doesn't correspond to a valid peer connection. + * Returns -2 if `public_key` is null. + */ +non_null(1) nullable(3) +int gc_get_peer_public_key_by_peer_id(const GC_Chat *chat, uint32_t peer_id, uint8_t *public_key); + +/** @brief Gets the connection status for peer associated with `peer_id`. + * + * Returns 2 if we have a direct (UDP) connection with a peer. + * Returns 1 if we have an indirect (TCP) connection with a peer. + * Returns 0 if peer_id is invalid or corresponds to ourselves. + * + * Note: Return values must correspond to Tox_Connection enum in API. + */ +non_null() +unsigned int gc_get_peer_connection_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the caller's status to `status`. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the packet failed to send. + */ +non_null() +int gc_set_self_status(const Messenger *m, int group_number, Group_Peer_Status status); + +/** @brief Returns the status of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The status returned is equal to the last status received through the status_change + * callback. + */ +non_null() +uint8_t gc_get_status(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Returns the group role of peer designated by `peer_id`. + * Returns UINT8_MAX on failure. + * + * The role returned is equal to the last role received through the moderation callback. + */ +non_null() +uint8_t gc_get_role(const GC_Chat *chat, uint32_t peer_id); + +/** @brief Sets the role of peer_id. role must be one of: GR_MODERATOR, GR_USER, GR_OBSERVER + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if caller does not have sufficient permissions for the action. + * Returns -4 if the role assignment is invalid. + * Returns -5 if the role failed to be set. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_set_peer_role(const Messenger *m, int group_number, uint32_t peer_id, Group_Role new_role); + +/** @brief Sets the group password and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If `password` is null or `password_length` is 0 the password will be unset for the group. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for the action. + * Returns -2 if the password is too long. + * Returns -3 if the packet failed to send. + * Returns -4 if malloc failed. + */ +non_null(1) nullable(2) +int gc_founder_set_password(GC_Chat *chat, const uint8_t *password, uint16_t password_length); + +/** @brief Sets the topic lock and distributes the new shared state to the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if `topic_lock` is an invalid type. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the group is disconnected. + * Returns -5 if the topic lock could not be set. + * Returns -6 if the packet failed to send. + */ +non_null() +int gc_founder_set_topic_lock(const Messenger *m, int group_number, Group_Topic_Lock new_lock_state); + +/** @brief Sets the group privacy state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the privacy state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_privacy_state(const Messenger *m, int group_number, Group_Privacy_State new_privacy_state); + +/** @brief Sets the group voice state and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * Returns 0 on success. + * Returns -1 if group_number is invalid. + * Returns -2 if the caller does not have sufficient permissions for this action. + * Returns -3 if the group is disconnected. + * Returns -4 if the voice state could not be set. + * Returns -5 if the packet failed to send. + */ +non_null() +int gc_founder_set_voice_state(const Messenger *m, int group_number, Group_Voice_State new_voice_state); + +/** @brief Sets the peer limit to maxpeers and distributes the new shared state to the group. + * + * This function requires that the shared state be re-signed and will only work for the group founder. + * + * Returns 0 on success. + * Returns -1 if the caller does not have sufficient permissions for this action. + * Returns -2 if the peer limit could not be set. + * Returns -3 if the packet failed to send. + */ +non_null() +int gc_founder_set_max_peers(GC_Chat *chat, uint16_t max_peers); + +/** @brief Removes peer designated by `peer_id` from peer list and sends a broadcast instructing + * all other peers to remove the peer from their peerlist as well. + * + * This function will not trigger the peer_exit callback for the caller. + * + * Returns 0 on success. + * Returns -1 if the group_number is invalid. + * Returns -2 if the peer_id is invalid. + * Returns -3 if the caller does not have sufficient permissions for this action. + * Returns -4 if the action failed. + * Returns -5 if the packet failed to send. + * Returns -6 if the caller attempted to kick himself. + */ +non_null() +int gc_kick_peer(const Messenger *m, int group_number, uint32_t peer_id); + +/** @brief Copies the chat_id to dest. If dest is null this function has no effect. + * + * `dest` should have room for at least CHAT_ID_SIZE bytes. + */ +non_null(1) nullable(2) +void gc_get_chat_id(const GC_Chat *chat, uint8_t *dest); + + +/** Group callbacks */ +non_null(1) nullable(2) void gc_callback_message(const Messenger *m, gc_message_cb *function); +non_null(1) nullable(2) void gc_callback_private_message(const Messenger *m, gc_private_message_cb *function); +non_null(1) nullable(2) void gc_callback_custom_packet(const Messenger *m, gc_custom_packet_cb *function); +non_null(1) nullable(2) void gc_callback_custom_private_packet(const Messenger *m, + gc_custom_private_packet_cb *function); +non_null(1) nullable(2) void gc_callback_moderation(const Messenger *m, gc_moderation_cb *function); +non_null(1) nullable(2) void gc_callback_nick_change(const Messenger *m, gc_nick_change_cb *function); +non_null(1) nullable(2) void gc_callback_status_change(const Messenger *m, gc_status_change_cb *function); +non_null(1) nullable(2) void gc_callback_topic_change(const Messenger *m, gc_topic_change_cb *function); +non_null(1) nullable(2) void gc_callback_peer_limit(const Messenger *m, gc_peer_limit_cb *function); +non_null(1) nullable(2) void gc_callback_privacy_state(const Messenger *m, gc_privacy_state_cb *function); +non_null(1) nullable(2) void gc_callback_topic_lock(const Messenger *m, gc_topic_lock_cb *function); +non_null(1) nullable(2) void gc_callback_password(const Messenger *m, gc_password_cb *function); +non_null(1) nullable(2) void gc_callback_peer_join(const Messenger *m, gc_peer_join_cb *function); +non_null(1) nullable(2) void gc_callback_peer_exit(const Messenger *m, gc_peer_exit_cb *function); +non_null(1) nullable(2) void gc_callback_self_join(const Messenger *m, gc_self_join_cb *function); +non_null(1) nullable(2) void gc_callback_rejected(const Messenger *m, gc_rejected_cb *function); +non_null(1) nullable(2) void gc_callback_voice_state(const Messenger *m, gc_voice_state_cb *function); + +/** @brief The main loop. Should be called with every Messenger iteration. */ +non_null(1) nullable(2) +void do_gc(GC_Session *c, void *userdata); + +/** + * Make sure that DHT is initialized before calling this. + * Returns a NULL pointer on failure. + */ +nullable(1) +GC_Session *new_dht_groupchats(Messenger *m); + +/** @brief Cleans up groupchat structures and calls `gc_group_exit()` for every group chat */ +nullable(1) +void kill_dht_groupchats(GC_Session *c); + +/** @brief Loads a previously saved group and attempts to join it. + * + * `bu` is the packed group info. + * + * Returns group_number on success. + * Returns -1 on failure. + */ +non_null() +int gc_group_load(GC_Session *c, Bin_Unpack *bu); + +/** + * @brief Saves info from `chat` to `bp` in binary format. + */ +non_null() +void gc_group_save(const GC_Chat *chat, Bin_Pack *bp); + +/** @brief Creates a new group and adds it to the group sessions group array. + * + * The caller of this function has founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * Return -1 if the nick or group name is too long. + * Return -2 if the nick or group name is empty. + * Return -3 if the the group object fails to initialize. + * Return -4 if the group state fails to initialize. + * Return -5 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_group_add(GC_Session *c, Group_Privacy_State privacy_state, const uint8_t *group_name, + uint16_t group_name_length, + const uint8_t *nick, size_t nick_length); + +/** @brief Joins a group designated by `chat_id`. + * + * This function creates a new GC_Chat object, adds it to the chats array, and sends a DHT + * announcement to find peers in the group associated with `chat_id`. Once a peer has been + * found a join attempt will be initiated. + * + * If the group is not password protected password should be set to NULL and password_length should be 0. + * + * Return group_number on success. + * Return -1 if the group object fails to initialize. + * Return -2 if chat_id is NULL or a group with chat_id already exists in the chats array. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the group password. + * Return -6 if the Messenger friend connection fails to initialize. + */ +non_null(1, 2, 3) nullable(5) +int gc_group_join(GC_Session *c, const uint8_t *chat_id, const uint8_t *nick, size_t nick_length, const uint8_t *passwd, + uint16_t passwd_len); + +/** @brief Disconnects from all peers in a group but saves the group state for later use. + * + * Return true on sucess. + * Return false if the group handler object or chat object is null. + */ +non_null() +bool gc_disconnect_from_group(const GC_Session *c, GC_Chat *chat); + +/** @brief Disconnects from all peers in a group and attempts to reconnect. + * + * All self state and credentials are retained. + * + * Returns 0 on success. + * Returns -1 if the group handler object or chat object is null. + * Returns -2 if the Messenger friend connection fails to initialize. + */ +non_null() +int gc_rejoin_group(GC_Session *c, GC_Chat *chat); + +/** @brief Joins a group using the invite data received in a friend's group invite. + * + * The invite is only valid while the inviter is present in the group. + * + * Return group_number on success. + * Return -1 if the invite data is malformed. + * Return -2 if the group object fails to initialize. + * Return -3 if nick is too long. + * Return -4 if nick is empty or nick length is zero. + * Return -5 if there is an error setting the password. + * Return -6 if friend doesn't exist. + * Return -7 if sending packet failed. + */ +non_null(1, 3, 5) nullable(7) +int gc_accept_invite(GC_Session *c, int32_t friend_number, const uint8_t *data, uint16_t length, const uint8_t *nick, + size_t nick_length, const uint8_t *passwd, uint16_t passwd_len); + +typedef bool gc_send_group_invite_packet_cb(const Messenger *m, uint32_t friendnumber, const uint8_t *packet, + uint16_t length); + +/** @brief Invites friend designated by `friendnumber` to chat. + * Packet includes: Type, chat_id, TCP node or packed IP_Port. + * + * Return 0 on success. + * Return -1 if friendnumber does not exist. + * Return -2 on failure to create the invite data. + * Return -3 if the packet fails to send. + */ +non_null() +int gc_invite_friend(const GC_Session *c, GC_Chat *chat, int32_t friend_number, + gc_send_group_invite_packet_cb *callback); + +/** @brief Leaves a group and sends an exit broadcast packet with an optional parting message. + * + * All group state is permanently lost, including keys and roles. + * + * Return 0 on success. + * Return -1 if the parting message is too long. + * Return -2 if the parting message failed to send. + */ +non_null(1, 2) nullable(3) +int gc_group_exit(GC_Session *c, GC_Chat *chat, const uint8_t *message, uint16_t length); + +/** @brief Returns true if `chat` is a valid group chat. + * + * A valid group chat constitutes an initialized chat instance with a non-zero shared state version. + * The shared state version will be non-zero either if a peer has created the group, or if + * they have ever successfully connected to the group. + */ +non_null() +bool gc_group_is_valid(const GC_Chat *chat); + +/** @brief Returns the number of active groups in `c`. */ +non_null() +uint32_t gc_count_groups(const GC_Session *c); + +/** @brief Returns true if peer_number exists */ +non_null() +bool gc_peer_number_is_valid(const GC_Chat *chat, int peer_number); + +/** @brief Return group_number's GC_Chat pointer on success + * Return NULL on failure + */ +non_null() +GC_Chat *gc_get_group(const GC_Session *c, int group_number); + +/** @brief Sends a lossy message acknowledgement to peer associated with `gconn`. + * + * If `type` is GR_ACK_RECV we send a read-receipt for read_id's packet. If `type` is GR_ACK_REQ + * we send a request for the respective id's packet. + * + * Requests are limited to one per second per peer. + * + * @retval true on success. + */ +non_null() +bool gc_send_message_ack(const GC_Chat *chat, GC_Connection *gconn, uint64_t message_id, Group_Message_Ack_Type type); + +/** @brief Helper function for `handle_gc_lossless_packet()`. + * + * Note: This function may modify the peer list and change peer numbers. + * + * @retval true if packet is successfully handled. + */ +non_null(1, 2) nullable(4, 7) +bool handle_gc_lossless_helper(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, const uint8_t *data, + uint16_t length, uint8_t packet_type, void *userdata); + +/** @brief Handles an invite accept packet. + * + * @retval true on success. + */ +non_null() +bool handle_gc_invite_accepted_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Return true if `chat_id` is not present in our group sessions array. + * + * `length` must be at least CHAT_ID_SIZE bytes in length. + */ +non_null() +bool group_not_added(const GC_Session *c, const uint8_t *chat_id, uint32_t length); + +/** @brief Handles an invite confirmed packet. + * + * Return 0 on success. + * Return -1 if length is invalid. + * Return -2 if data contains invalid chat_id. + * Return -3 if data contains invalid peer info. + * Return -4 if `friend_number` does not designate a valid friend. + * Return -5 if data contains invalid connection info. + */ +non_null() +int handle_gc_invite_confirmed_packet(const GC_Session *c, int friend_number, const uint8_t *data, uint16_t length); + +/** @brief Returns the group designated by `public_key`. + * Returns null if group does not exist. + */ +non_null() +GC_Chat *gc_get_group_by_public_key(const GC_Session *c, const uint8_t *public_key); + +/** @brief Attempts to add peers from `announces` to our peer list and initiate an invite request. + * + * Returns the number of peers added on success. + * Returns -1 on failure. + */ +non_null() +int gc_add_peers_from_announces(GC_Chat *chat, const GC_Announce *announces, uint8_t gc_announces_count); + +#endif // GROUP_CHATS_H diff --git a/toxcore/group_common.h b/toxcore/group_common.h new file mode 100644 index 0000000000..8ee0795543 --- /dev/null +++ b/toxcore/group_common.h @@ -0,0 +1,403 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2022 The TokTok team. + */ + +/** + * Common groupchat data structures. + */ + +#ifndef GROUP_COMMON_H +#define GROUP_COMMON_H + +#include +#include + +#include "DHT.h" +#include "TCP_connection.h" +#include "group_moderation.h" + +#define MAX_GC_PART_MESSAGE_SIZE 128 +#define MAX_GC_NICK_SIZE 128 +#define MAX_GC_TOPIC_SIZE 512 +#define MAX_GC_GROUP_NAME_SIZE 48 +#define GC_MESSAGE_PSEUDO_ID_SIZE 4 +#define GROUP_MAX_MESSAGE_LENGTH 1368 + +#define MAX_GC_MESSAGE_SIZE GROUP_MAX_MESSAGE_LENGTH +#define MAX_GC_MESSAGE_RAW_SIZE (MAX_GC_MESSAGE_SIZE + GC_MESSAGE_PSEUDO_ID_SIZE) +#define MAX_GC_CUSTOM_PACKET_SIZE 1373 +#define MAX_GC_PASSWORD_SIZE 32 +#define MAX_GC_SAVED_INVITES 10 +#define MAX_GC_PEERS_DEFAULT 100 +#define MAX_GC_SAVED_TIMEOUTS 12 +#define GC_MAX_SAVED_PEERS 100 +#define GC_SAVED_PEER_SIZE (ENC_PUBLIC_KEY_SIZE + sizeof(Node_format) + sizeof(IP_Port)) + +/* Max size of a packet chunk. Packets larger than this must be split up. */ +#define MAX_GC_PACKET_CHUNK_SIZE 500 + +/* Max size of a complete encrypted packet including headers. */ +#define MAX_GC_PACKET_SIZE (MAX_GC_PACKET_CHUNK_SIZE * 100) + +/* Max number of messages to store in the send/recv arrays */ +#define GCC_BUFFER_SIZE 8192 + +/** Self UDP status. Must correspond to return values from `ipport_self_copy()`. */ +typedef enum Self_UDP_Status { + SELF_UDP_STATUS_NONE = 0x00, + SELF_UDP_STATUS_WAN = 0x01, + SELF_UDP_STATUS_LAN = 0x02, +} Self_UDP_Status; + +/** Group exit types. */ +typedef enum Group_Exit_Type { + GC_EXIT_TYPE_QUIT = 0x00, // Peer left the group + GC_EXIT_TYPE_TIMEOUT = 0x01, // Peer connection timed out + GC_EXIT_TYPE_DISCONNECTED = 0x02, // Peer diconnected from group + GC_EXIT_TYPE_SELF_DISCONNECTED = 0x03, // Self disconnected from group + GC_EXIT_TYPE_KICKED = 0x04, // Peer was kicked from the group + GC_EXIT_TYPE_SYNC_ERR = 0x05, // Peer failed to sync with the group + GC_EXIT_TYPE_NO_CALLBACK = 0x06, // The peer exit callback should not be triggered +} Group_Exit_Type; + +typedef struct GC_Exit_Info { + uint8_t part_message[MAX_GC_PART_MESSAGE_SIZE]; + uint16_t length; + Group_Exit_Type exit_type; +} GC_Exit_Info; + +typedef struct GC_PeerAddress { + uint8_t public_key[EXT_PUBLIC_KEY_SIZE]; + IP_Port ip_port; +} GC_PeerAddress; + +typedef struct GC_Message_Array_Entry { + uint8_t *data; + uint16_t data_length; + uint8_t packet_type; + uint64_t message_id; + uint64_t time_added; + uint64_t last_send_try; +} GC_Message_Array_Entry; + +typedef struct GC_Connection { + uint64_t send_message_id; /* message_id of the next message we send to peer */ + + uint16_t send_array_start; /* send_array index of oldest item */ + GC_Message_Array_Entry *send_array; + + uint64_t received_message_id; /* message_id of peer's last message to us */ + GC_Message_Array_Entry *recv_array; + + uint64_t last_chunk_id; /* The message ID of the last packet fragment we received */ + + GC_PeerAddress addr; /* holds peer's extended real public key and ip_port */ + uint32_t public_key_hash; /* Jenkins one at a time hash of peer's real encryption public key */ + + uint8_t session_public_key[ENC_PUBLIC_KEY_SIZE]; /* self session public key for this peer */ + uint8_t session_secret_key[ENC_SECRET_KEY_SIZE]; /* self session secret key for this peer */ + uint8_t session_shared_key[CRYPTO_SHARED_KEY_SIZE]; /* made with our session sk and peer's session pk */ + + int tcp_connection_num; + uint64_t last_sent_tcp_relays_time; /* the last time we attempted to send this peer our tcp relays */ + uint16_t tcp_relay_share_index; + uint64_t last_received_direct_time; /* the last time we received a direct UDP packet from this connection */ + uint64_t last_sent_ip_time; /* the last time we sent our ip info to this peer in a ping packet */ + + Node_format connected_tcp_relays[MAX_FRIEND_TCP_CONNECTIONS]; + uint16_t tcp_relays_count; + + uint64_t last_received_packet_time; /* The last time we successfully processed any packet from this peer */ + uint64_t last_requested_packet_time; /* The last time we requested a missing packet from this peer */ + uint64_t last_sent_ping_time; + uint64_t last_sync_response; /* the last time we sent this peer a sync response */ + uint8_t oob_relay_pk[CRYPTO_PUBLIC_KEY_SIZE]; + bool self_is_closer; /* true if we're "closer" to the chat_id than this peer (uses real pk's) */ + + bool confirmed; /* true if this peer has given us their info */ + bool handshaked; /* true if we've successfully handshaked with this peer */ + uint16_t handshake_attempts; + uint64_t last_handshake_request; + uint64_t last_handshake_response; + uint8_t pending_handshake_type; + bool is_pending_handshake_response; + bool is_oob_handshake; + + uint64_t last_key_rotation; /* the last time we rotated session keys for this peer */ + bool pending_key_rotation_request; + + bool pending_delete; /* true if this peer has been marked for deletion */ + GC_Exit_Info exit_info; +} GC_Connection; + +/*** + * Group roles. Roles are hierarchical in that each role has a set of privileges plus + * all the privileges of the roles below it. + */ +typedef enum Group_Role { + /** Group creator. All-powerful. Cannot be demoted or kicked. */ + GR_FOUNDER = 0x00, + + /** + * May promote or demote peers below them to any role below them. + * May also kick peers below them and set the topic. + */ + GR_MODERATOR = 0x01, + + /** may interact normally with the group. */ + GR_USER = 0x02, + + /** May not interact with the group but may observe. */ + GR_OBSERVER = 0x03, +} Group_Role; + +typedef enum Group_Peer_Status { + GS_NONE = 0x00, + GS_AWAY = 0x01, + GS_BUSY = 0x02, +} Group_Peer_Status; + +/** + * Group voice states. The state determines which Group Roles have permission to speak. + */ +typedef enum Group_Voice_State { + /** Every group role except Observers may speak. */ + GV_ALL = 0x00, + + /** Only Moderators and the Founder may speak. */ + GV_MODS = 0x01, + + /** Only the Founder may speak. */ + GV_FOUNDER = 0x02, +} Group_Voice_State; + +/** Group connection states. */ +typedef enum GC_Conn_State { + CS_NONE = 0x00, // Indicates a group is not initialized + CS_DISCONNECTED = 0x01, // Not receiving or sending any packets + CS_CONNECTING = 0x02, // Attempting to establish a connection with peers in the group + CS_CONNECTED = 0x03, // Has successfully received a sync response from a peer in the group +} GC_Conn_State; + +/** Group privacy states. */ +typedef enum Group_Privacy_State { + GI_PUBLIC = 0x00, // Anyone with the chat ID may join the group + GI_PRIVATE = 0x01, // Peers may only join the group via a friend invite +} Group_Privacy_State; + +/** Handshake join types. */ +typedef enum Group_Handshake_Join_Type { + HJ_PUBLIC = 0x00, // Indicates the group was joined via the DHT + HJ_PRIVATE = 0x01, // Indicates the group was joined via private friend invite +} Group_Handshake_Join_Type; + +typedef struct GC_SavedPeerInfo { + uint8_t public_key[ENC_PUBLIC_KEY_SIZE]; + Node_format tcp_relay; + IP_Port ip_port; +} GC_SavedPeerInfo; + +/** Holds info about peers who recently timed out */ +typedef struct GC_TimedOutPeer { + GC_SavedPeerInfo addr; + uint64_t last_seen; // the time the peer disconnected + uint64_t last_reconn_try; // the last time we tried to establish a new connection +} GC_TimedOutPeer; + +typedef struct GC_Peer { + /* Below state is sent to other peers in peer info exchange */ + uint8_t nick[MAX_GC_NICK_SIZE]; + uint16_t nick_length; + uint8_t status; + + /* Below state is local only */ + Group_Role role; + uint32_t peer_id; // permanent ID (used for the public API) + bool ignore; + + GC_Connection gconn; +} GC_Peer; + +typedef struct GC_SharedState { + uint32_t version; + uint8_t founder_public_key[EXT_PUBLIC_KEY_SIZE]; + uint16_t maxpeers; + uint16_t group_name_len; + uint8_t group_name[MAX_GC_GROUP_NAME_SIZE]; + Group_Privacy_State privacy_state; // GI_PUBLIC (uses DHT) or GI_PRIVATE (invite only) + uint16_t password_length; + uint8_t password[MAX_GC_PASSWORD_SIZE]; + uint8_t mod_list_hash[MOD_MODERATION_HASH_SIZE]; + uint32_t topic_lock; // equal to GC_TOPIC_LOCK_ENABLED when lock is enabled + Group_Voice_State voice_state; +} GC_SharedState; + +typedef struct GC_TopicInfo { + uint32_t version; + uint16_t length; + uint16_t checksum; // used for syncing problems. the checksum with the highest value gets priority. + uint8_t topic[MAX_GC_TOPIC_SIZE]; + uint8_t public_sig_key[SIG_PUBLIC_KEY_SIZE]; // Public signature key of the topic setter +} GC_TopicInfo; + +typedef struct GC_Chat { + Mono_Time *mono_time; + const Logger *log; + const Random *rng; + + Self_UDP_Status self_udp_status; + IP_Port self_ip_port; + + Networking_Core *net; + TCP_Connections *tcp_conn; + + uint32_t tcp_connections; // the number of global TCP relays we're connected to + uint64_t last_checked_tcp_relays; + Group_Handshake_Join_Type join_type; + + GC_Peer *group; + Moderation moderation; + + GC_Conn_State connection_state; + + GC_SharedState shared_state; + uint8_t shared_state_sig[SIGNATURE_SIZE]; // signed by founder using the chat secret key + + GC_TopicInfo topic_info; + uint8_t topic_sig[SIGNATURE_SIZE]; // signed by the peer who set the current topic + uint16_t topic_prev_checksum; // checksum of the previous topic + uint64_t topic_time_set; + + uint16_t peers_checksum; // sum of the public key hash of every confirmed peer in the group + uint16_t roles_checksum; // sum of every confirmed peer's role plus the first byte of their public key + + uint32_t numpeers; + int group_number; + + uint8_t chat_public_key[EXT_PUBLIC_KEY_SIZE]; // the chat_id is the sig portion + uint8_t chat_secret_key[EXT_SECRET_KEY_SIZE]; // only used by the founder + + uint8_t self_public_key[EXT_PUBLIC_KEY_SIZE]; + uint8_t self_secret_key[EXT_SECRET_KEY_SIZE]; + + uint64_t time_connected; + uint64_t last_ping_interval; + uint64_t last_sync_request; // The last time we sent a sync request to any peer + uint64_t last_sync_response_peer_list; // The last time we sent the peer list to any peer + uint64_t last_time_peers_loaded; + + /* keeps track of frequency of new inbound connections */ + uint8_t connection_O_metre; + uint64_t connection_cooldown_timer; + bool block_handshakes; + + int32_t saved_invites[MAX_GC_SAVED_INVITES]; + uint8_t saved_invites_index; + + /** A list of recently seen peers in case we disconnect from a private group. + * Peers are added once they're confirmed, and only if there are vacant + * spots (older connections get priority). An entry is removed only when the list + * is full, its respective peer goes offline, and an online peer who isn't yet + * present in the list can be added. + */ + GC_SavedPeerInfo saved_peers[GC_MAX_SAVED_PEERS]; + + GC_TimedOutPeer timeout_list[MAX_GC_SAVED_TIMEOUTS]; + size_t timeout_list_index; + uint64_t last_timed_out_reconn_try; // the last time we tried to reconnect to timed out peers + + bool update_self_announces; // true if we should try to update our announcements + uint64_t last_self_announce_check; // the last time we checked if we should update our announcements + + uint8_t m_group_public_key[CRYPTO_PUBLIC_KEY_SIZE]; // public key for group's messenger friend connection + int friend_connection_id; // identifier for group's messenger friend connection +} GC_Chat; + +#ifndef MESSENGER_DEFINED +#define MESSENGER_DEFINED +typedef struct Messenger Messenger; +#endif /* MESSENGER_DEFINED */ + +typedef void gc_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *data, size_t length, uint32_t message_id, void *user_data); +typedef void gc_private_message_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *data, size_t length, void *user_data); +typedef void gc_custom_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_custom_private_packet_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, + size_t length, void *user_data); +typedef void gc_moderation_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, uint32_t target_peer, + unsigned int mod_event, void *user_data); +typedef void gc_nick_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_status_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int status, + void *user_data); +typedef void gc_topic_change_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); +typedef void gc_topic_lock_cb(const Messenger *m, uint32_t group_number, unsigned int topic_lock, void *user_data); +typedef void gc_voice_state_cb(const Messenger *m, uint32_t group_number, unsigned int voice_state, void *user_data); +typedef void gc_peer_limit_cb(const Messenger *m, uint32_t group_number, uint32_t max_peers, void *user_data); +typedef void gc_privacy_state_cb(const Messenger *m, uint32_t group_number, unsigned int state, void *user_data); +typedef void gc_password_cb(const Messenger *m, uint32_t group_number, const uint8_t *data, size_t length, + void *user_data); +typedef void gc_peer_join_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data); +typedef void gc_peer_exit_cb(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int exit_type, + const uint8_t *nick, size_t nick_len, const uint8_t *data, size_t length, void *user_data); +typedef void gc_self_join_cb(const Messenger *m, uint32_t group_number, void *user_data); +typedef void gc_rejected_cb(const Messenger *m, uint32_t group_number, unsigned int type, void *user_data); + +typedef struct GC_Session { + Messenger *messenger; + GC_Chat *chats; + struct GC_Announces_List *announces_list; + + uint32_t chats_index; + + gc_message_cb *message; + gc_private_message_cb *private_message; + gc_custom_packet_cb *custom_packet; + gc_custom_private_packet_cb *custom_private_packet; + gc_moderation_cb *moderation; + gc_nick_change_cb *nick_change; + gc_status_change_cb *status_change; + gc_topic_change_cb *topic_change; + gc_topic_lock_cb *topic_lock; + gc_voice_state_cb *voice_state; + gc_peer_limit_cb *peer_limit; + gc_privacy_state_cb *privacy_state; + gc_password_cb *password; + gc_peer_join_cb *peer_join; + gc_peer_exit_cb *peer_exit; + gc_self_join_cb *self_join; + gc_rejected_cb *rejected; +} GC_Session; + +/** @brief Adds a new peer to group_number's peer list. + * + * Return peer_number on success. + * Return -1 on failure. + * Return -2 if a peer with public_key is already in our peerlist. + */ +non_null(1, 3) nullable(2) +int peer_add(GC_Chat *chat, const IP_Port *ipp, const uint8_t *public_key); + +/** @brief Unpacks saved peers from `data` of size `length` into `chat`. + * + * Returns the number of unpacked peers on success. + * Returns -1 on failure. + */ +non_null() +int unpack_gc_saved_peers(GC_Chat *chat, const uint8_t *data, uint16_t length); + +/** @brief Packs all valid entries from saved peerlist into `data`. + * + * If `processed` is non-null it will be set to the length of the packed data. + * + * Return the number of packed saved peers on success. + * Return -1 if buffer is too small. + */ +non_null(1, 2) nullable(4) +int pack_gc_saved_peers(const GC_Chat *chat, uint8_t *data, uint16_t length, uint16_t *processed); + +#endif // GROUP_COMMON_H diff --git a/toxcore/group_connection.c b/toxcore/group_connection.c new file mode 100644 index 0000000000..86c353c00c --- /dev/null +++ b/toxcore/group_connection.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#include "group_connection.h" + +#include +#include +#include +#include + +#include "DHT.h" +#include "ccompat.h" +#include "crypto_core.h" +#include "group_chats.h" +#include "group_common.h" +#include "mono_time.h" +#include "util.h" + +#ifndef VANILLA_NACL + +/** Seconds since last direct UDP packet was received before the connection is considered dead */ +#define GCC_UDP_DIRECT_TIMEOUT (GC_PING_TIMEOUT + 4) + +/** Returns true if array entry does not contain an active packet. */ +non_null() +static bool array_entry_is_empty(const GC_Message_Array_Entry *array_entry) +{ + assert(array_entry != nullptr); + return array_entry->time_added == 0; +} + +/** @brief Clears an array entry. */ +non_null() +static void clear_array_entry(GC_Message_Array_Entry *const array_entry) +{ + if (array_entry->data != nullptr) { + free(array_entry->data); + } + + *array_entry = (GC_Message_Array_Entry) { + nullptr + }; +} + +/** + * Clears every send array message from queue starting at the index designated by + * `start_id` and ending at `end_id`, and sets the send_message_id for `gconn` + * to `start_id`. + */ +non_null() +static void clear_send_queue_id_range(GC_Connection *gconn, uint64_t start_id, uint64_t end_id) +{ + const uint16_t start_idx = gcc_get_array_index(start_id); + const uint16_t end_idx = gcc_get_array_index(end_id); + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + GC_Message_Array_Entry *entry = &gconn->send_array[i]; + clear_array_entry(entry); + } + + gconn->send_message_id = start_id; +} + +uint16_t gcc_get_array_index(uint64_t message_id) +{ + return message_id % GCC_BUFFER_SIZE; +} + +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id) +{ + gconn->send_message_id = id; + gconn->send_array_start = id % GCC_BUFFER_SIZE; +} + +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id) +{ + gconn->received_message_id = id; +} + +/** @brief Puts packet data in array_entry. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +static bool create_array_entry(const Mono_Time *mono_time, GC_Message_Array_Entry *array_entry, const uint8_t *data, + uint16_t length, uint8_t packet_type, uint64_t message_id) +{ + if (length > 0) { + if (data == nullptr) { + return false; + } + + array_entry->data = (uint8_t *)malloc(sizeof(uint8_t) * length); + + if (array_entry->data == nullptr) { + return false; + } + + memcpy(array_entry->data, data, length); + } + + const uint64_t tm = mono_time_get(mono_time); + + array_entry->data_length = length; + array_entry->packet_type = packet_type; + array_entry->message_id = message_id; + array_entry->time_added = tm; + array_entry->last_send_try = tm; + + return true; +} + +/** @brief Adds data of length to gconn's send_array. + * + * Returns true on success and increments gconn's send_message_id. + */ +non_null(1, 2, 3) nullable(4) +static bool add_to_send_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + /* check if send_array is full */ + if ((gconn->send_message_id % GCC_BUFFER_SIZE) == (uint16_t)(gconn->send_array_start - 1)) { + LOGGER_DEBUG(log, "Send array overflow"); + return false; + } + + const uint16_t idx = gcc_get_array_index(gconn->send_message_id); + GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + + if (!array_entry_is_empty(array_entry)) { + LOGGER_DEBUG(log, "Send array entry isn't empty"); + return false; + } + + if (!create_array_entry(mono_time, array_entry, data, length, packet_type, gconn->send_message_id)) { + LOGGER_WARNING(log, "Failed to create array entry"); + return false; + } + + ++gconn->send_message_id; + + return true; +} + +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type) +{ + const uint64_t message_id = gconn->send_message_id; + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, data, length, packet_type)) { + LOGGER_WARNING(chat->log, "Failed to add payload to send array: (type: 0x%02x, length: %d)", packet_type, length); + return -1; + } + + if (!gcc_encrypt_and_send_lossless_packet(chat, gconn, data, length, message_id, packet_type)) { + LOGGER_DEBUG(chat->log, "Failed to send payload: (type: 0x%02x, length: %d)", packet_type, length); + return -2; + } + + return 0; +} + + +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type) +{ + if (length <= MAX_GC_PACKET_CHUNK_SIZE || data == nullptr) { + LOGGER_FATAL(chat->log, "invalid length or null data pointer"); + return false; + } + + const uint16_t start_id = gconn->send_message_id; + + // First packet segment is comprised of packet type + first chunk of payload + uint8_t chunk[MAX_GC_PACKET_CHUNK_SIZE]; + chunk[0] = packet_type; + memcpy(chunk + 1, data, MAX_GC_PACKET_CHUNK_SIZE - 1); + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, MAX_GC_PACKET_CHUNK_SIZE, GP_FRAGMENT)) { + return false; + } + + uint16_t processed = MAX_GC_PACKET_CHUNK_SIZE - 1; + + // The rest of the segments are added in chunks + while (length > processed) { + const uint16_t chunk_len = min_u16(MAX_GC_PACKET_CHUNK_SIZE, length - processed); + + memcpy(chunk, data + processed, chunk_len); + processed += chunk_len; + + if (!add_to_send_array(chat->log, chat->mono_time, gconn, chunk, chunk_len, GP_FRAGMENT)) { + clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); + return false; + } + } + + // empty packet signals the end of the sequence + if (!add_to_send_array(chat->log, chat->mono_time, gconn, nullptr, 0, GP_FRAGMENT)) { + clear_send_queue_id_range(gconn, start_id, gconn->send_message_id); + return false; + } + + const uint16_t start_idx = gcc_get_array_index(start_id); + const uint16_t end_idx = gcc_get_array_index(gconn->send_message_id); + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + GC_Message_Array_Entry *entry = &gconn->send_array[i]; + + if (array_entry_is_empty(entry)) { + LOGGER_FATAL(chat->log, "array entry for packet chunk is empty"); + return false; + } + + assert(entry->packet_type == GP_FRAGMENT); + + gcc_encrypt_and_send_lossless_packet(chat, gconn, entry->data, entry->data_length, + entry->message_id, entry->packet_type); + } + + return true; +} + +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id) +{ + uint16_t idx = gcc_get_array_index(message_id); + GC_Message_Array_Entry *array_entry = &gconn->send_array[idx]; + + if (array_entry_is_empty(array_entry)) { + return true; + } + + if (array_entry->message_id != message_id) { // wrap-around indicates a connection problem + LOGGER_DEBUG(log, "Wrap-around on message %llu", (unsigned long long)message_id); + return false; + } + + clear_array_entry(array_entry); + + /* Put send_array_start in proper position */ + if (idx == gconn->send_array_start) { + const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + + while (array_entry_is_empty(&gconn->send_array[idx]) && gconn->send_array_start != end) { + gconn->send_array_start = (gconn->send_array_start + 1) % GCC_BUFFER_SIZE; + idx = (idx + 1) % GCC_BUFFER_SIZE; + } + } + + return true; +} + +bool gcc_ip_port_is_set(const GC_Connection *gconn) +{ + return ipport_isset(&gconn->addr.ip_port); +} + +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp) +{ + if (ipp != nullptr && ipport_isset(ipp)) { + gconn->addr.ip_port = *ipp; + } +} + +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn) +{ + if (gconn == nullptr || tcp_node == nullptr) { + return false; + } + + if (gconn->tcp_relays_count == 0) { + return false; + } + + const uint32_t rand_idx = random_range_u32(rng, gconn->tcp_relays_count); + + if (!ipport_isset(&gconn->connected_tcp_relays[rand_idx].ip_port)) { + return false; + } + + *tcp_node = gconn->connected_tcp_relays[rand_idx]; + + return true; +} + +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node) +{ + if (gconn == nullptr || tcp_node == nullptr) { + return -1; + } + + if (!ipport_isset(&tcp_node->ip_port)) { + return -1; + } + + for (uint16_t i = 0; i < gconn->tcp_relays_count; ++i) { + if (pk_equal(gconn->connected_tcp_relays[i].public_key, tcp_node->public_key)) { + return -2; + } + } + + uint32_t idx = gconn->tcp_relays_count; + + if (gconn->tcp_relays_count >= MAX_FRIEND_TCP_CONNECTIONS) { + idx = random_range_u32(rng, gconn->tcp_relays_count); + } else { + ++gconn->tcp_relays_count; + } + + gconn->connected_tcp_relays[idx] = *tcp_node; + + return 0; +} + +/** @brief Stores `data` of length `length` in the receive array for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3) nullable(4) +static bool store_in_recv_array(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, + uint16_t length, uint8_t packet_type, uint64_t message_id) +{ + const uint16_t idx = gcc_get_array_index(message_id); + GC_Message_Array_Entry *ary_entry = &gconn->recv_array[idx]; + + if (!array_entry_is_empty(ary_entry)) { + LOGGER_DEBUG(log, "Recv array is not empty"); + return false; + } + + if (!create_array_entry(mono_time, ary_entry, data, length, packet_type, message_id)) { + LOGGER_WARNING(log, "Failed to create array entry"); + return false; + } + + return true; +} + +/** + * Reassembles a fragmented packet sequence ending with the data in the receive + * array at slot `message_id - 1` and starting with the last found slot containing + * a GP_FRAGMENT packet when searching backwards in the array. + * + * The fully reassembled packet is stored in `payload`, which must be passed as a + * null pointer, and must be free'd by the caller. + * + * Return the length of the fully reassembled packet on success. + * Return 0 on failure. + */ +non_null(1, 3) nullable(2) +static uint16_t reassemble_packet(const Logger *log, GC_Connection *gconn, uint8_t **payload, uint64_t message_id) +{ + uint16_t end_idx = gcc_get_array_index(message_id - 1); + uint16_t start_idx = end_idx; + uint16_t packet_length = 0; + + GC_Message_Array_Entry *entry = &gconn->recv_array[end_idx]; + + // search backwards in recv array until we find an empty slot or a non-fragment packet type + while (!array_entry_is_empty(entry) && entry->packet_type == GP_FRAGMENT) { + assert(entry->data != nullptr); + assert(entry->data_length <= MAX_GC_PACKET_CHUNK_SIZE); + + const uint16_t diff = packet_length + entry->data_length; + + assert(diff > packet_length); // overflow check + packet_length = diff; + + if (packet_length > MAX_GC_PACKET_SIZE) { + LOGGER_ERROR(log, "Payload of size %u exceeded max packet size", packet_length); // should never happen + return 0; + } + + start_idx = start_idx > 0 ? start_idx - 1 : GCC_BUFFER_SIZE - 1; + entry = &gconn->recv_array[start_idx]; + + if (start_idx == end_idx) { + LOGGER_ERROR(log, "Packet reassemble wrap-around"); + return 0; + } + } + + if (packet_length == 0) { + return 0; + } + + assert(*payload == nullptr); + *payload = (uint8_t *)malloc(packet_length); + + if (*payload == nullptr) { + LOGGER_ERROR(log, "Failed to allocate %u bytes for payload buffer", packet_length); + return 0; + } + + start_idx = (start_idx + 1) % GCC_BUFFER_SIZE; + end_idx = (end_idx + 1) % GCC_BUFFER_SIZE; + + uint16_t processed = 0; + + for (uint16_t i = start_idx; i != end_idx; i = (i + 1) % GCC_BUFFER_SIZE) { + entry = &gconn->recv_array[i]; + + assert(processed + entry->data_length <= packet_length); + memcpy(*payload + processed, entry->data, entry->data_length); + processed += entry->data_length; + + clear_array_entry(entry); + } + + return processed; +} + +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, + GC_Connection *gconn, const uint8_t *chunk, uint16_t length, uint8_t packet_type, + uint64_t message_id, void *userdata) +{ + if (length > 0) { + if (!store_in_recv_array(chat->log, chat->mono_time, gconn, chunk, length, packet_type, message_id)) { + return -1; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + gconn->last_chunk_id = message_id; + + return 1; + } + + uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; + memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + + uint8_t *payload = nullptr; + const uint16_t processed_len = reassemble_packet(chat->log, gconn, &payload, message_id); + + if (processed_len == 0) { + free(payload); + return -1; + } + + if (!handle_gc_lossless_helper(c, chat, peer_number, payload + 1, processed_len - 1, payload[0], userdata)) { + free(payload); + return -1; + } + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + if (gconn == nullptr) { + return 0; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + gconn->last_chunk_id = 0; + + free(payload); + + return 0; +} + +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + bool direct_conn) +{ + if (direct_conn) { + gconn->last_received_direct_time = mono_time_get(mono_time); + } + + /* Appears to be a duplicate packet so we discard it */ + if (message_id < gconn->received_message_id + 1) { + return 0; + } + + if (packet_type == GP_FRAGMENT) { // we handle packet fragments as a special case + return 3; + } + + /* we're missing an older message from this peer so we store it in recv_array */ + if (message_id > gconn->received_message_id + 1) { + if (!store_in_recv_array(log, mono_time, gconn, data, length, packet_type, message_id)) { + return -1; + } + + return 1; + } + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + + return 2; +} + +/** @brief Handles peer_number's array entry with appropriate handler and clears it from array. + * + * This function increments the received message ID for `gconn`. + * + * Return true on success. + */ +non_null(1, 2, 3, 5) nullable(6) +static bool process_recv_array_entry(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + GC_Message_Array_Entry *const array_entry, void *userdata) +{ + uint8_t sender_pk[ENC_PUBLIC_KEY_SIZE]; + memcpy(sender_pk, get_enc_key(gconn->addr.public_key), ENC_PUBLIC_KEY_SIZE); + + const bool ret = handle_gc_lossless_helper(c, chat, peer_number, array_entry->data, array_entry->data_length, + array_entry->packet_type, userdata); + + /* peer number can change from peer add operations in packet handlers */ + peer_number = get_peer_number_of_enc_pk(chat, sender_pk, false); + gconn = get_gc_connection(chat, peer_number); + + clear_array_entry(array_entry); + + if (gconn == nullptr) { + return true; + } + + if (!ret) { + gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_REQ); + return false; + } + + gc_send_message_ack(chat, gconn, array_entry->message_id, GR_ACK_RECV); + + gcc_set_recv_message_id(gconn, gconn->received_message_id + 1); + + return true; +} + +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + void *userdata) +{ + if (gconn->last_chunk_id != 0) { // dont check array if we have an unfinished fragment sequence + return; + } + + const uint16_t idx = (gconn->received_message_id + 1) % GCC_BUFFER_SIZE; + GC_Message_Array_Entry *const array_entry = &gconn->recv_array[idx]; + + if (!array_entry_is_empty(array_entry)) { + process_recv_array_entry(c, chat, gconn, peer_number, array_entry, userdata); + } +} + +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn) +{ + const uint64_t tm = mono_time_get(chat->mono_time); + const uint16_t start = gconn->send_array_start; + const uint16_t end = gconn->send_message_id % GCC_BUFFER_SIZE; + + GC_Message_Array_Entry *array_entry = &gconn->send_array[start]; + + if (array_entry_is_empty(array_entry)) { + return; + } + + if (mono_time_is_timeout(chat->mono_time, array_entry->time_added, GC_CONFIRMED_PEER_TIMEOUT)) { + gcc_mark_for_deletion(gconn, chat->tcp_conn, GC_EXIT_TYPE_TIMEOUT, nullptr, 0); + LOGGER_DEBUG(chat->log, "Send array stuck; timing out peer"); + return; + } + + for (uint16_t i = start; i != end; i = (i + 1) % GCC_BUFFER_SIZE) { + array_entry = &gconn->send_array[i]; + + if (array_entry_is_empty(array_entry)) { + continue; + } + + if (tm == array_entry->last_send_try) { + continue; + } + + const uint64_t delta = array_entry->last_send_try - array_entry->time_added; + array_entry->last_send_try = tm; + + /* if this occurrs less than once per second this won't be reliable */ + if (delta > 1 && is_power_of_2(delta)) { + gcc_encrypt_and_send_lossless_packet(chat, gconn, array_entry->data, array_entry->data_length, + array_entry->message_id, array_entry->packet_type); + } + } +} + +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length) +{ + if (packet == nullptr || length == 0) { + return false; + } + + bool direct_send_attempt = false; + + if (gcc_direct_conn_is_possible(chat, gconn)) { + if (gcc_conn_is_direct(chat->mono_time, gconn)) { + return (uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length; + } + + if ((uint16_t) sendpacket(chat->net, &gconn->addr.ip_port, packet, length) == length) { + direct_send_attempt = true; + } + } + + const int ret = send_packet_tcp_connection(chat->tcp_conn, gconn->tcp_connection_num, packet, length); + return ret == 0 || direct_send_attempt; +} + +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint64_t message_id, uint8_t packet_type) +{ + const uint16_t packet_size = gc_get_wrapped_packet_size(length, NET_PACKET_GC_LOSSLESS); + uint8_t *packet = (uint8_t *)malloc(packet_size); + + if (packet == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packet buffer"); + return false; + } + + const int enc_len = group_packet_wrap( + chat->log, chat->rng, chat->self_public_key, gconn->session_shared_key, packet, + packet_size, data, length, message_id, packet_type, NET_PACKET_GC_LOSSLESS); + + if (enc_len < 0) { + LOGGER_ERROR(chat->log, "Failed to wrap packet (type: 0x%02x, error: %d)", packet_type, enc_len); + free(packet); + return false; + } + + if (!gcc_send_packet(chat, gconn, packet, (uint16_t)enc_len)) { + LOGGER_DEBUG(chat->log, "Failed to send packet (type: 0x%02x, enc_len: %d)", packet_type, enc_len); + free(packet); + return false; + } + + free(packet); + + return true; +} + +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk) +{ + encrypt_precompute(sender_pk, gconn->session_secret_key, gconn->session_shared_key); +} + +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn) +{ + return GCC_UDP_DIRECT_TIMEOUT + gconn->last_received_direct_time > mono_time_get(mono_time); +} + +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn) +{ + return !net_family_is_unspec(gconn->addr.ip_port.ip.family) && !net_family_is_unspec(net_family(chat->net)); +} + +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, + const uint8_t *part_message, uint16_t length) +{ + if (gconn == nullptr) { + return; + } + + if (gconn->pending_delete) { + return; + } + + gconn->pending_delete = true; + gconn->exit_info.exit_type = type; + + kill_tcp_connection_to(tcp_conn, gconn->tcp_connection_num); + + if (length > 0 && length <= MAX_GC_PART_MESSAGE_SIZE && part_message != nullptr) { + memcpy(gconn->exit_info.part_message, part_message, length); + gconn->exit_info.length = length; + } +} + +void gcc_peer_cleanup(GC_Connection *gconn) +{ + for (size_t i = 0; i < GCC_BUFFER_SIZE; ++i) { + free(gconn->send_array[i].data); + free(gconn->recv_array[i].data); + } + + free(gconn->recv_array); + free(gconn->send_array); + + crypto_memunlock(gconn->session_secret_key, sizeof(gconn->session_secret_key)); + crypto_memunlock(gconn->session_shared_key, sizeof(gconn->session_shared_key)); + crypto_memzero(gconn, sizeof(GC_Connection)); +} + +void gcc_cleanup(const GC_Chat *chat) +{ + for (uint32_t i = 0; i < chat->numpeers; ++i) { + GC_Connection *gconn = get_gc_connection(chat, i); + assert(gconn != nullptr); + + gcc_peer_cleanup(gconn); + } +} + +#endif // VANILLA_NACL diff --git a/toxcore/group_connection.h b/toxcore/group_connection.h new file mode 100644 index 0000000000..2202c7ab36 --- /dev/null +++ b/toxcore/group_connection.h @@ -0,0 +1,189 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * An implementation of massive text only group chats. + */ + +#ifndef GROUP_CONNECTION_H +#define GROUP_CONNECTION_H + +#include "group_common.h" + +/* Max number of TCP relays we share with a peer on handshake */ +#define GCC_MAX_TCP_SHARED_RELAYS 3 + +/** Marks a peer for deletion. If gconn is null or already marked for deletion this function has no effect. */ +non_null(1, 2) nullable(4) +void gcc_mark_for_deletion(GC_Connection *gconn, TCP_Connections *tcp_conn, Group_Exit_Type type, + const uint8_t *part_message, uint16_t length); + +/** @brief Decides if message need to be put in recv_array or immediately handled. + * + * Return 3 if message is in correct sequence and is a fragment packet. + * Return 2 if message is in correct sequence and may be handled immediately. + * Return 1 if packet is out of sequence and added to recv_array. + * Return 0 if message is a duplicate. + * Return -1 on failure + */ +non_null(1, 2, 3) nullable(4) +int gcc_handle_received_message(const Logger *log, const Mono_Time *mono_time, GC_Connection *gconn, + const uint8_t *data, uint16_t length, uint8_t packet_type, uint64_t message_id, + bool direct_conn); + +/** @brief Handles a packet fragment. + * + * If the fragment is incomplete, it gets stored in the recv + * array. Otherwise the segment is re-assembled into a complete + * payload and processed. + * + * Return 1 if fragment is successfully handled and is not the end of the sequence. + * Return 0 if fragment is the end of a sequence and successfully handled. + * Return -1 on failure. + */ +non_null(1, 2, 4) nullable(5, 9) +int gcc_handle_packet_fragment(const GC_Session *c, GC_Chat *chat, uint32_t peer_number, GC_Connection *gconn, + const uint8_t *chunk, uint16_t length, uint8_t packet_type, uint64_t message_id, + void *userdata); + +/** @brief Return array index for message_id */ +uint16_t gcc_get_array_index(uint64_t message_id); + +/** @brief Removes send_array item with message_id. + * + * Return true on success. + */ +non_null() +bool gcc_handle_ack(const Logger *log, GC_Connection *gconn, uint64_t message_id); + +/** @brief Sets the send_message_id and send_array_start for `gconn` to `id`. + * + * This should only be used to initialize a new lossless connection. + */ +non_null() +void gcc_set_send_message_id(GC_Connection *gconn, uint64_t id); + +/** @brief Sets the received_message_id for `gconn` to `id`. */ +non_null() +void gcc_set_recv_message_id(GC_Connection *gconn, uint64_t id); + +/** + * @brief Returns true if the ip_port is set for gconn. + */ +non_null() +bool gcc_ip_port_is_set(const GC_Connection *gconn); + +/** + * @brief Sets the ip_port for gconn to ipp. + * + * If ipp is not set this function has no effect. + */ +non_null(1) nullable(2) +void gcc_set_ip_port(GC_Connection *gconn, const IP_Port *ipp); + +/** @brief Copies a random TCP relay node from gconn to tcp_node. + * + * Return true on success. + */ +non_null() +bool gcc_copy_tcp_relay(const Random *rng, Node_format *tcp_node, const GC_Connection *gconn); + +/** @brief Saves tcp_node to gconn's list of connected tcp relays. + * + * If relays list is full a random node is overwritten with the new node. + * + * Return 0 on success. + * Return -1 on failure. + * Return -2 if node is already in list. + */ +non_null() +int gcc_save_tcp_relay(const Random *rng, GC_Connection *gconn, const Node_format *tcp_node); + +/** @brief Checks for and handles messages that are in proper sequence in gconn's recv_array. + * This should always be called after a new packet is successfully handled. + */ +non_null(1, 2, 3) nullable(5) +void gcc_check_recv_array(const GC_Session *c, GC_Chat *chat, GC_Connection *gconn, uint32_t peer_number, + void *userdata); + +/** @brief Attempts to re-send lossless packets that have not yet received an ack. */ +non_null() +void gcc_resend_packets(const GC_Chat *chat, GC_Connection *gconn); + +/** + * Uses public encryption key `sender_pk` and the shared secret key associated with `gconn` + * to generate a shared 32-byte encryption key that can be used by the owners of both keys for symmetric + * encryption and decryption. + * + * Puts the result in the shared session key buffer for `gconn`, which must have room for + * CRYPTO_SHARED_KEY_SIZE bytes. This resulting shared key should be treated as a secret key. + */ +non_null() +void gcc_make_session_shared_key(GC_Connection *gconn, const uint8_t *sender_pk); + +/** @brief Return true if we have a direct connection with `gconn`. */ +non_null() +bool gcc_conn_is_direct(const Mono_Time *mono_time, const GC_Connection *gconn); + +/** @brief Return true if a direct UDP connection is possible with `gconn`. */ +non_null() +bool gcc_direct_conn_is_possible(const GC_Chat *chat, const GC_Connection *gconn); + +/** @brief Sends a packet to the peer associated with gconn. + * + * This is a lower level function that does not encrypt or wrap the packet. + * + * Return true on success. + */ +non_null() +bool gcc_send_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *packet, uint16_t length); + +/** @brief Sends a lossless packet to `gconn` comprised of `data` of size `length`. + * + * This function will add the packet to the lossless send array, encrypt/wrap it using the + * shared key associated with `gconn`, and send it over the wire. + * + * Return 0 on success. + * Return -1 if the packet couldn't be added to the send array. + * Return -2 if the packet failed to be encrypted or failed to send. + */ +non_null(1, 2) nullable(3) +int gcc_send_lossless_packet(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, uint16_t length, + uint8_t packet_type); + +/** @brief Splits a lossless packet up into fragments, wraps each fragment in a GP_FRAGMENT + * header, encrypts them, and send them in succession. + * + * This function will first try to add each packet fragment to the send array as an atomic + * unit. If any chunk fails to be added the process will be reversed and an error will be + * returned. Otherwise it will then try to send all the fragments in succession. + * + * Return true if all fragments are successfully added to the send array. + */ +non_null() +bool gcc_send_lossless_packet_fragments(const GC_Chat *chat, GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint8_t packet_type); + + +/** @brief Encrypts `data` of `length` bytes, designated by `message_id`, using the shared key + * associated with `gconn` and sends lossless packet over the wire. + * + * This function does not add the packet to the send array. + * + * Return true on success. + */ +non_null(1, 2) nullable(3) +bool gcc_encrypt_and_send_lossless_packet(const GC_Chat *chat, const GC_Connection *gconn, const uint8_t *data, + uint16_t length, uint64_t message_id, uint8_t packet_type); + +/** @brief Called when a peer leaves the group. */ +non_null() +void gcc_peer_cleanup(GC_Connection *gconn); + +/** @brief Called on group exit. */ +non_null() +void gcc_cleanup(const GC_Chat *chat); + +#endif // GROUP_CONNECTION_H diff --git a/toxcore/group_moderation.c b/toxcore/group_moderation.c index dea38a648b..b16c397cbe 100644 --- a/toxcore/group_moderation.c +++ b/toxcore/group_moderation.c @@ -27,6 +27,10 @@ static_assert(MOD_MAX_NUM_SANCTIONS * MOD_SANCTION_PACKED_SIZE + MOD_SANCTIONS_C "MOD_MAX_NUM_SANCTIONS must be able to fit inside the maximum allowed payload size"); static_assert(MOD_MAX_NUM_MODERATORS * MOD_LIST_ENTRY_SIZE <= MAX_PACKET_SIZE_NO_HEADERS, "MOD_MAX_NUM_MODERATORS must be able to fit insize the maximum allowed payload size"); +static_assert(MOD_MAX_NUM_MODERATORS <= MOD_MAX_NUM_MODERATORS_LIMIT, + "MOD_MAX_NUM_MODERATORS must be <= MOD_MAX_NUM_MODERATORS_LIMIT"); +static_assert(MOD_MAX_NUM_SANCTIONS <= MOD_MAX_NUM_SANCTIONS_LIMIT, + "MOD_MAX_NUM_SANCTIONS must be <= MOD_MAX_NUM_SANCTIONS_LIMIT"); uint16_t mod_list_packed_size(const Moderation *moderation) { @@ -398,7 +402,7 @@ int sanctions_list_unpack(Mod_Sanction *sanctions, Mod_Sanction_Creds *creds, ui */ non_null(4) nullable(1) static bool sanctions_list_make_hash(const Mod_Sanction *sanctions, uint32_t new_version, uint16_t num_sanctions, - uint8_t *hash) + uint8_t *hash) { if (num_sanctions == 0 || sanctions == nullptr) { memset(hash, 0, MOD_SANCTION_HASH_SIZE); @@ -568,7 +572,7 @@ bool sanctions_list_check_integrity(const Moderation *moderation, const Mod_Sanc } /** @brief Validates a sanctions list if credentials are supplied. If successful, - * or if no credentials are supplid, assigns new sanctions list and credentials + * or if no credentials are supplied, assigns new sanctions list and credentials * to moderation object. * * @param moderation The moderation object being operated on. diff --git a/toxcore/group_moderation.h b/toxcore/group_moderation.h index 36b44a42f0..8b2a73869a 100644 --- a/toxcore/group_moderation.h +++ b/toxcore/group_moderation.h @@ -36,9 +36,17 @@ extern "C" { /* The max size of a groupchat packet with 100 bytes reserved for header data */ #define MAX_PACKET_SIZE_NO_HEADERS 49900 -/* These values must take into account the maximum allowed packet size and headers. */ -#define MOD_MAX_NUM_MODERATORS (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) -#define MOD_MAX_NUM_SANCTIONS (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) +/* The maximum possible number of moderators that can be sent in a group packet sequence. */ +#define MOD_MAX_NUM_MODERATORS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE))) + +/* The maximum number of moderators that we allow in a group: 100 */ +#define MOD_MAX_NUM_MODERATORS ((MOD_MAX_NUM_MODERATORS_LIMIT / 16) + 3) + +/* The maximum number of sanctions that be sent in a group packet sequence. */ +#define MOD_MAX_NUM_SANCTIONS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE))) + +/* The maximum number of sanctions that we allow in a group: 30 */ +#define MOD_MAX_NUM_SANCTIONS (MOD_MAX_NUM_SANCTIONS_LIMIT / 12) typedef enum Mod_Sanction_Type { SA_OBSERVER = 0x00, diff --git a/toxcore/group_onion_announce.c b/toxcore/group_onion_announce.c new file mode 100644 index 0000000000..b797770e52 --- /dev/null +++ b/toxcore/group_onion_announce.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#include "group_onion_announce.h" + +#include +#include + +#include "ccompat.h" + +static_assert(GCA_ANNOUNCE_MAX_SIZE <= ONION_MAX_EXTRA_DATA_SIZE, + "GC_Announce does not fit into the onion packet extra data"); + +static pack_extra_data_cb pack_group_announces; +non_null() +static int pack_group_announces(void *object, const Logger *logger, const Mono_Time *mono_time, + uint8_t num_nodes, uint8_t *plain, uint16_t plain_size, + uint8_t *response, uint16_t response_size, uint16_t offset) +{ + GC_Announces_List *gc_announces_list = (GC_Announces_List *)object; + GC_Public_Announce public_announce; + + if (gca_unpack_public_announce(logger, plain, plain_size, + &public_announce) == -1) { + LOGGER_WARNING(logger, "Failed to unpack public group announce"); + return -1; + } + + const GC_Peer_Announce *new_announce = gca_add_announce(mono_time, gc_announces_list, &public_announce); + + if (new_announce == nullptr) { + LOGGER_ERROR(logger, "Failed to add group announce"); + return -1; + } + + GC_Announce gc_announces[GCA_MAX_SENT_ANNOUNCES]; + const int num_ann = gca_get_announces(gc_announces_list, + gc_announces, + GCA_MAX_SENT_ANNOUNCES, + public_announce.chat_public_key, + new_announce->base_announce.peer_public_key); + + if (num_ann < 0) { + LOGGER_ERROR(logger, "failed to get group announce"); + return -1; + } + + assert(num_ann <= UINT8_MAX); + + size_t announces_length = 0; + + if (gca_pack_announces_list(logger, response + offset, response_size - offset, gc_announces, (uint8_t)num_ann, + &announces_length) != num_ann) { + LOGGER_WARNING(logger, "Failed to pack group announces list"); + return -1; + } + + return announces_length; +} + +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a) +{ + onion_announce_extra_data_callback(onion_a, GCA_MAX_SENT_ANNOUNCES * sizeof(GC_Announce), pack_group_announces, + group_announce); +} + +#ifndef VANILLA_NACL + +int create_gca_announce_request( + const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, + const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, + const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, + const uint8_t *gc_data, uint16_t gc_data_length) +{ + if (max_packet_length < ONION_ANNOUNCE_REQUEST_MAX_SIZE || gc_data_length == 0) { + return -1; + } + + uint8_t plain[ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + GCA_ANNOUNCE_MAX_SIZE]; + uint8_t *position_in_plain = plain; + const size_t encrypted_size = sizeof(plain) - GCA_ANNOUNCE_MAX_SIZE + gc_data_length; + + memcpy(plain, ping_id, ONION_PING_ID_SIZE); + position_in_plain += ONION_PING_ID_SIZE; + + memcpy(position_in_plain, client_id, CRYPTO_PUBLIC_KEY_SIZE); + position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + + memcpy(position_in_plain, data_public_key, CRYPTO_PUBLIC_KEY_SIZE); + position_in_plain += CRYPTO_PUBLIC_KEY_SIZE; + + memcpy(position_in_plain, &sendback_data, sizeof(sendback_data)); + position_in_plain += sizeof(sendback_data); + + memcpy(position_in_plain, gc_data, gc_data_length); + + packet[0] = NET_PACKET_ANNOUNCE_REQUEST; + random_nonce(rng, packet + 1); + memcpy(packet + 1 + CRYPTO_NONCE_SIZE, public_key, CRYPTO_PUBLIC_KEY_SIZE); + + const int len = encrypt_data(dest_client_id, secret_key, packet + 1, plain, + encrypted_size, packet + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE); + + const uint32_t full_length = (uint32_t)len + 1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE; + + if (full_length != ONION_ANNOUNCE_REQUEST_MIN_SIZE + gc_data_length) { + return -1; + } + + return full_length; +} +#endif // VANILLA_NACL diff --git a/toxcore/group_onion_announce.h b/toxcore/group_onion_announce.h new file mode 100644 index 0000000000..5c6d64ec59 --- /dev/null +++ b/toxcore/group_onion_announce.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +#ifndef C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H +#define C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H + +#include "group_announce.h" +#include "onion_announce.h" + +non_null() +void gca_onion_init(GC_Announces_List *group_announce, Onion_Announce *onion_a); + +non_null() +int create_gca_announce_request( + const Random *rng, uint8_t *packet, uint16_t max_packet_length, const uint8_t *dest_client_id, + const uint8_t *public_key, const uint8_t *secret_key, const uint8_t *ping_id, + const uint8_t *client_id, const uint8_t *data_public_key, uint64_t sendback_data, + const uint8_t *gc_data, uint16_t gc_data_length); + +#endif // C_TOXCORE_TOXCORE_GROUP_ONION_ANNOUNCE_H diff --git a/toxcore/group_pack.c b/toxcore/group_pack.c new file mode 100644 index 0000000000..ecdd965610 --- /dev/null +++ b/toxcore/group_pack.c @@ -0,0 +1,423 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#include "group_pack.h" + +#include +#include +#include +#include + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "ccompat.h" +#include "util.h" + +non_null() +static bool load_unpack_state_values(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 8)) { + LOGGER_ERROR(chat->log, "Group state values array malformed"); + return false; + } + + bool manually_disconnected = false; + uint8_t privacy_state = 0; + uint8_t voice_state = 0; + + if (!(bin_unpack_bool(bu, &manually_disconnected) + && bin_unpack_u16(bu, &chat->shared_state.group_name_len) + && bin_unpack_u08(bu, &privacy_state) + && bin_unpack_u16(bu, &chat->shared_state.maxpeers) + && bin_unpack_u16(bu, &chat->shared_state.password_length) + && bin_unpack_u32(bu, &chat->shared_state.version) + && bin_unpack_u32(bu, &chat->shared_state.topic_lock) + && bin_unpack_u08(bu, &voice_state))) { + LOGGER_ERROR(chat->log, "Failed to unpack state value"); + return false; + } + + chat->connection_state = manually_disconnected ? CS_DISCONNECTED : CS_CONNECTING; + chat->shared_state.privacy_state = (Group_Privacy_State)privacy_state; + chat->shared_state.voice_state = (Group_Voice_State)voice_state; + + // we always load saved groups as private in case the group became private while we were offline. + // this will have no detrimental effect if the group is public, as the correct privacy + // state will be set via sync. + chat->join_type = HJ_PRIVATE; + + return true; +} + +non_null() +static bool load_unpack_state_bin(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 5)) { + LOGGER_ERROR(chat->log, "Group state binary array malformed"); + return false; + } + + if (!(bin_unpack_bin_fixed(bu, chat->shared_state_sig, SIGNATURE_SIZE) + && bin_unpack_bin_fixed(bu, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->shared_state.group_name, chat->shared_state.group_name_len) + && bin_unpack_bin_fixed(bu, chat->shared_state.password, chat->shared_state.password_length) + && bin_unpack_bin_fixed(bu, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack state binary data"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_topic_info(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 6)) { + LOGGER_ERROR(chat->log, "Group topic array malformed"); + return false; + } + + if (!(bin_unpack_u32(bu, &chat->topic_info.version) + && bin_unpack_u16(bu, &chat->topic_info.length) + && bin_unpack_u16(bu, &chat->topic_info.checksum) + && bin_unpack_bin_fixed(bu, chat->topic_info.topic, chat->topic_info.length) + && bin_unpack_bin_fixed(bu, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->topic_sig, SIGNATURE_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack topic info"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_mod_list(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 2)) { + LOGGER_ERROR(chat->log, "Group mod list array malformed"); + return false; + } + + if (!bin_unpack_u16(bu, &chat->moderation.num_mods)) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list value"); + return false; + } + + if (chat->moderation.num_mods == 0) { + bin_unpack_nil(bu); + return true; + } + + if (chat->moderation.num_mods > MOD_MAX_NUM_MODERATORS) { + LOGGER_ERROR(chat->log, "moderation count %u exceeds maximum %u", chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); + return false; + } + + uint8_t *packed_mod_list = (uint8_t *)malloc(chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE); + + if (packed_mod_list == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for packed mod list"); + return false; + } + + const size_t packed_size = chat->moderation.num_mods * MOD_LIST_ENTRY_SIZE; + + if (!bin_unpack_bin_fixed(bu, packed_mod_list, packed_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list binary data"); + free(packed_mod_list); + return false; + } + + if (mod_list_unpack(&chat->moderation, packed_mod_list, packed_size, chat->moderation.num_mods) == -1) { + LOGGER_ERROR(chat->log, "Failed to unpack mod list info"); + free(packed_mod_list); + return false; + } + + free(packed_mod_list); + + return true; +} + +non_null() +static bool load_unpack_keys(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 4)) { + LOGGER_ERROR(chat->log, "Group keys array malformed"); + return false; + } + + if (!(bin_unpack_bin_fixed(bu, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->chat_secret_key, EXT_SECRET_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->self_public_key, EXT_PUBLIC_KEY_SIZE) + && bin_unpack_bin_fixed(bu, chat->self_secret_key, EXT_SECRET_KEY_SIZE))) { + LOGGER_ERROR(chat->log, "Failed to unpack keys"); + return false; + } + + return true; +} + +non_null() +static bool load_unpack_self_info(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 4)) { + LOGGER_ERROR(chat->log, "Group self info array malformed"); + return false; + } + + uint8_t self_nick[MAX_GC_NICK_SIZE]; + uint16_t self_nick_len = 0; + uint8_t self_role = GR_USER; + uint8_t self_status = GS_NONE; + + if (!(bin_unpack_u16(bu, &self_nick_len) + && bin_unpack_u08(bu, &self_role) + && bin_unpack_u08(bu, &self_status))) { + LOGGER_ERROR(chat->log, "Failed to unpack self values"); + return false; + } + + assert(self_nick_len <= MAX_GC_NICK_SIZE); + + if (!bin_unpack_bin_fixed(bu, self_nick, self_nick_len)) { + LOGGER_ERROR(chat->log, "Failed to unpack self nick bytes"); + return false; + } + + // we have to add ourself before setting self info + if (peer_add(chat, nullptr, chat->self_public_key) != 0) { + LOGGER_ERROR(chat->log, "Failed to add self to peer list"); + return false; + } + + assert(chat->numpeers > 0); + + GC_Peer *self = &chat->group[0]; + + memcpy(self->gconn.addr.public_key, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); + memcpy(self->nick, self_nick, self_nick_len); + self->nick_length = self_nick_len; + self->role = (Group_Role)self_role; + self->status = (Group_Peer_Status)self_status; + self->gconn.confirmed = true; + + return true; +} + +non_null() +static bool load_unpack_saved_peers(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 2)) { + LOGGER_ERROR(chat->log, "Group saved peers array malformed"); + return false; + } + + // Saved peers + uint16_t saved_peers_size = 0; + + if (!bin_unpack_u16(bu, &saved_peers_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers value"); + return false; + } + + if (saved_peers_size == 0) { + bin_unpack_nil(bu); + return true; + } + + uint8_t *saved_peers = (uint8_t *)malloc(saved_peers_size * GC_SAVED_PEER_SIZE); + + if (saved_peers == nullptr) { + LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peer list"); + return false; + } + + if (!bin_unpack_bin_fixed(bu, saved_peers, saved_peers_size)) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers binary data"); + free(saved_peers); + return false; + } + + if (unpack_gc_saved_peers(chat, saved_peers, saved_peers_size) == -1) { + LOGGER_ERROR(chat->log, "Failed to unpack saved peers"); // recoverable error + } + + free(saved_peers); + + return true; +} + +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu) +{ + if (!bin_unpack_array_fixed(bu, 7)) { + LOGGER_ERROR(chat->log, "Group info array malformed"); + return false; + } + + return load_unpack_state_values(chat, bu) + && load_unpack_state_bin(chat, bu) + && load_unpack_topic_info(chat, bu) + && load_unpack_mod_list(chat, bu) + && load_unpack_keys(chat, bu) + && load_unpack_self_info(chat, bu) + && load_unpack_saved_peers(chat, bu); +} + +non_null() +static void save_pack_state_values(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 8); + bin_pack_bool(bp, chat->connection_state == CS_DISCONNECTED); // 1 + bin_pack_u16(bp, chat->shared_state.group_name_len); // 2 + bin_pack_u08(bp, chat->shared_state.privacy_state); // 3 + bin_pack_u16(bp, chat->shared_state.maxpeers); // 4 + bin_pack_u16(bp, chat->shared_state.password_length); // 5 + bin_pack_u32(bp, chat->shared_state.version); // 6 + bin_pack_u32(bp, chat->shared_state.topic_lock); // 7 + bin_pack_u08(bp, chat->shared_state.voice_state); // 8 +} + +non_null() +static void save_pack_state_bin(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 5); + + bin_pack_bin(bp, chat->shared_state_sig, SIGNATURE_SIZE); // 1 + bin_pack_bin(bp, chat->shared_state.founder_public_key, EXT_PUBLIC_KEY_SIZE); // 2 + bin_pack_bin(bp, chat->shared_state.group_name, chat->shared_state.group_name_len); // 3 + bin_pack_bin(bp, chat->shared_state.password, chat->shared_state.password_length); // 4 + bin_pack_bin(bp, chat->shared_state.mod_list_hash, MOD_MODERATION_HASH_SIZE); // 5 +} + +non_null() +static void save_pack_topic_info(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 6); + + bin_pack_u32(bp, chat->topic_info.version); // 1 + bin_pack_u16(bp, chat->topic_info.length); // 2 + bin_pack_u16(bp, chat->topic_info.checksum); // 3 + bin_pack_bin(bp, chat->topic_info.topic, chat->topic_info.length); // 4 + bin_pack_bin(bp, chat->topic_info.public_sig_key, SIG_PUBLIC_KEY_SIZE); // 5 + bin_pack_bin(bp, chat->topic_sig, SIGNATURE_SIZE); // 6 +} + +non_null() +static void save_pack_mod_list(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 2); + + const uint16_t num_mods = min_u16(chat->moderation.num_mods, MOD_MAX_NUM_MODERATORS); + + if (num_mods == 0) { + bin_pack_u16(bp, num_mods); // 1 + bin_pack_nil(bp); // 2 + return; + } + + uint8_t *packed_mod_list = (uint8_t *)malloc(num_mods * MOD_LIST_ENTRY_SIZE); + + // we can still recover without the mod list + if (packed_mod_list == nullptr) { + bin_pack_u16(bp, 0); // 1 + bin_pack_nil(bp); // 2 + LOGGER_ERROR(chat->log, "Failed to allocate memory for moderation list"); + return; + } + + bin_pack_u16(bp, num_mods); // 1 + + mod_list_pack(&chat->moderation, packed_mod_list); + + const size_t packed_size = num_mods * MOD_LIST_ENTRY_SIZE; + + bin_pack_bin(bp, packed_mod_list, packed_size); // 2 + + free(packed_mod_list); +} + +non_null() +static void save_pack_keys(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 4); + + bin_pack_bin(bp, chat->chat_public_key, EXT_PUBLIC_KEY_SIZE); // 1 + bin_pack_bin(bp, chat->chat_secret_key, EXT_SECRET_KEY_SIZE); // 2 + bin_pack_bin(bp, chat->self_public_key, EXT_PUBLIC_KEY_SIZE); // 3 + bin_pack_bin(bp, chat->self_secret_key, EXT_SECRET_KEY_SIZE); // 4 +} + +non_null() +static void save_pack_self_info(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 4); + + const GC_Peer *self = &chat->group[0]; + + assert(self->nick_length <= MAX_GC_NICK_SIZE); + + bin_pack_u16(bp, self->nick_length); // 1 + bin_pack_u08(bp, (uint8_t)self->role); // 2 + bin_pack_u08(bp, (uint8_t)self->status); // 3 + bin_pack_bin(bp, self->nick, self->nick_length); // 4 +} + +non_null() +static void save_pack_saved_peers(const GC_Chat *chat, Bin_Pack *bp) +{ + bin_pack_array(bp, 2); + + uint8_t *saved_peers = (uint8_t *)malloc(GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE); + + // we can still recover without the saved peers list + if (saved_peers == nullptr) { + bin_pack_u16(bp, 0); // 1 + bin_pack_nil(bp); // 2 + LOGGER_ERROR(chat->log, "Failed to allocate memory for saved peers list"); + return; + } + + uint16_t packed_size = 0; + const int count = pack_gc_saved_peers(chat, saved_peers, GC_MAX_SAVED_PEERS * GC_SAVED_PEER_SIZE, &packed_size); + + if (count < 0) { + LOGGER_ERROR(chat->log, "Failed to pack saved peers"); + } + + bin_pack_u16(bp, packed_size); // 1 + + if (packed_size == 0) { + bin_pack_nil(bp); // 2 + free(saved_peers); + return; + } + + bin_pack_bin(bp, saved_peers, packed_size); // 2 + + free(saved_peers); +} + +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp) +{ + if (chat->numpeers == 0) { + LOGGER_ERROR(chat->log, "Failed to pack group: numpeers is 0"); + return; + } + + bin_pack_array(bp, 7); + + save_pack_state_values(chat, bp); // 1 + save_pack_state_bin(chat, bp); // 2 + save_pack_topic_info(chat, bp); // 3 + save_pack_mod_list(chat, bp); // 4 + save_pack_keys(chat, bp); // 5 + save_pack_self_info(chat, bp); // 6 + save_pack_saved_peers(chat, bp); // 7 +} diff --git a/toxcore/group_pack.h b/toxcore/group_pack.h new file mode 100644 index 0000000000..ae831ac708 --- /dev/null +++ b/toxcore/group_pack.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2016-2020 The TokTok team. + * Copyright © 2015 Tox project. + */ + +/** + * Packer and unpacker functions for saving and loading groups. + */ + +#ifndef GROUP_PACK_H +#define GROUP_PACK_H + +#include + +#include "bin_pack.h" +#include "bin_unpack.h" +#include "group_common.h" + +/** + * Packs group data from `chat` into `mp` in binary format. Parallel to the + * `gc_load_unpack_group` function. + */ +non_null() +void gc_save_pack_group(const GC_Chat *chat, Bin_Pack *bp); + +/** + * Unpacks binary group data from `obj` into `chat`. Parallel to the `gc_save_pack_group` + * function. + * + * Return true if unpacking is successful. + */ +non_null() +bool gc_load_unpack_group(GC_Chat *chat, Bin_Unpack *bu); + +#endif // GROUP_PACK_H diff --git a/toxcore/net_crypto.c b/toxcore/net_crypto.c index c34a45734c..f649df460e 100644 --- a/toxcore/net_crypto.c +++ b/toxcore/net_crypto.c @@ -231,7 +231,7 @@ static int create_cookie_request(const Net_Crypto *c, uint8_t *packet, const uin memcpy(packet + 1, dht_get_self_public_key(c->dht), CRYPTO_PUBLIC_KEY_SIZE); memcpy(packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, nonce, CRYPTO_NONCE_SIZE); const int len = encrypt_data_symmetric(shared_key, nonce, plain, sizeof(plain), - packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); + packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE); if (len != COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE) { return -1; @@ -343,8 +343,8 @@ static int handle_cookie_request(const Net_Crypto *c, uint8_t *request_plain, ui memcpy(dht_public_key, packet + 1, CRYPTO_PUBLIC_KEY_SIZE); dht_get_shared_key_sent(c->dht, shared_key, dht_public_key); const int len = decrypt_data_symmetric(shared_key, packet + 1 + CRYPTO_PUBLIC_KEY_SIZE, - packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, - request_plain); + packet + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_NONCE_SIZE, COOKIE_REQUEST_PLAIN_LENGTH + CRYPTO_MAC_SIZE, + request_plain); if (len != COOKIE_REQUEST_PLAIN_LENGTH) { return -1; @@ -488,7 +488,7 @@ static int create_crypto_handshake(const Net_Crypto *c, uint8_t *packet, const u random_nonce(c->rng, packet + 1 + COOKIE_LENGTH); const int len = encrypt_data(peer_real_pk, c->self_secret_key, packet + 1 + COOKIE_LENGTH, plain, sizeof(plain), - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); if (len != HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE)) { return -1; @@ -541,8 +541,8 @@ static bool handle_crypto_handshake(const Net_Crypto *c, uint8_t *nonce, uint8_t uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; const int len = decrypt_data(cookie_plain, c->self_secret_key, packet + 1 + COOKIE_LENGTH, - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, - HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, + HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); if (len != sizeof(plain)) { return false; @@ -1273,7 +1273,7 @@ static int handle_data_packet(const Net_Crypto *c, int crypt_connection_id, uint const uint16_t diff = num - num_cur_nonce; increment_nonce_number(nonce, diff); const int len = decrypt_data_symmetric(conn->shared_key, nonce, packet + 1 + sizeof(uint16_t), - length - (1 + sizeof(uint16_t)), data); + length - (1 + sizeof(uint16_t)), data); if ((unsigned int)len != length - crypto_packet_overhead) { return -1; @@ -1515,7 +1515,7 @@ static void connection_kill(Net_Crypto *c, int crypt_connection_id, void *userda if (conn->connection_status_callback != nullptr) { conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, - false, userdata); + false, userdata); } while (true) { /* TODO(irungentoo): is this really the best way to do this? */ @@ -1602,7 +1602,7 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const if (conn->connection_status_callback != nullptr) { conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, - true, userdata); + true, userdata); } } @@ -1616,8 +1616,8 @@ static int handle_data_packet_core(Net_Crypto *c, int crypt_connection_id, const } const int requested = handle_request_packet(c->mono_time, &conn->send_array, - real_data, real_length, - &rtt_calc_time, rtt_time); + real_data, real_length, + &rtt_calc_time, rtt_time); if (requested == -1) { return -1; @@ -2657,7 +2657,7 @@ static void send_crypto_packets(Net_Crypto *c) &conn->recv_array) + 1.0) / (conn->packet_recv_rate + 1.0)); const double request_packet_interval2 = ((CRYPTO_PACKET_MIN_RATE / conn->packet_recv_rate) * - (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL; + (double)CRYPTO_SEND_PACKET_INTERVAL) + (double)PACKET_COUNTER_AVERAGE_INTERVAL; if (request_packet_interval2 < request_packet_interval) { request_packet_interval = request_packet_interval2; @@ -2750,7 +2750,7 @@ static void send_crypto_packets(Net_Crypto *c) PACKET_COUNTER_AVERAGE_INTERVAL)); const double min_speed_request = 1000.0 * (((double)(total_sent + total_resent)) / ( - (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL)); + (double)CONGESTION_QUEUE_ARRAY_SIZE * PACKET_COUNTER_AVERAGE_INTERVAL)); if (min_speed < CRYPTO_PACKET_MIN_RATE) { min_speed = CRYPTO_PACKET_MIN_RATE; diff --git a/toxcore/net_crypto.h b/toxcore/net_crypto.h index f6e062f3be..0c3dfd0d45 100644 --- a/toxcore/net_crypto.h +++ b/toxcore/net_crypto.h @@ -59,6 +59,7 @@ #define PACKET_ID_FILE_SENDREQUEST 80 #define PACKET_ID_FILE_CONTROL 81 #define PACKET_ID_FILE_DATA 82 +#define PACKET_ID_INVITE_GROUPCHAT 95 #define PACKET_ID_INVITE_CONFERENCE 96 #define PACKET_ID_ONLINE_PACKET 97 #define PACKET_ID_DIRECT_CONFERENCE 98 diff --git a/toxcore/network.c b/toxcore/network.c index 5c179bfa47..db4f2dec25 100644 --- a/toxcore/network.c +++ b/toxcore/network.c @@ -685,18 +685,17 @@ static const char *net_packet_type_name(Net_Packet_Type type) case NET_PACKET_CRYPTO: return "CRYPTO"; - case NET_PACKET_LAN_DISCOVERY: - return "LAN_DISCOVERY"; + case NET_PACKET_GC_HANDSHAKE: + return "GC_HANDSHAKE"; - // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats -// case NET_PACKET_GC_HANDSHAKE: -// return "GC_HANDSHAKE"; + case NET_PACKET_GC_LOSSLESS: + return "GC_LOSSLESS"; -// case NET_PACKET_GC_LOSSLESS: -// return "GC_LOSSLESS"; + case NET_PACKET_GC_LOSSY: + return "GC_LOSSY"; -// case NET_PACKET_GC_LOSSY: -// return "GC_LOSSY"; + case NET_PACKET_LAN_DISCOVERY: + return "LAN_DISCOVERY"; case NET_PACKET_ONION_SEND_INITIAL: return "ONION_SEND_INITIAL"; @@ -1933,6 +1932,12 @@ uint16_t net_ntohs(uint16_t hostshort) return ntohs(hostshort); } +size_t net_pack_bool(uint8_t *bytes, bool v) +{ + bytes[0] = v ? 1 : 0; + return 1; +} + size_t net_pack_u16(uint8_t *bytes, uint16_t v) { bytes[0] = (v >> 8) & 0xff; @@ -1956,6 +1961,12 @@ size_t net_pack_u64(uint8_t *bytes, uint64_t v) return p - bytes; } +size_t net_unpack_bool(const uint8_t *bytes, bool *v) +{ + *v = bytes[0] != 0; + return 1; +} + size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v) { const uint8_t hi = bytes[0]; diff --git a/toxcore/network.h b/toxcore/network.h index a1fc6a74fc..5a9226221d 100644 --- a/toxcore/network.h +++ b/toxcore/network.h @@ -109,10 +109,9 @@ typedef enum Net_Packet_Type { NET_PACKET_CRYPTO = 0x24, /* Encrypted data packet ID. */ NET_PACKET_LAN_DISCOVERY = 0x25, /* LAN discovery packet ID. */ - // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats - // NET_PACKET_GC_HANDSHAKE = 0x62, /* Group chat handshake packet ID */ - // NET_PACKET_GC_LOSSLESS = 0x63, /* Group chat lossless packet ID */ - // NET_PACKET_GC_LOSSY = 0x64, /* Group chat lossy packet ID */ + NET_PACKET_GC_HANDSHAKE = 0x62, /* Group chat handshake packet ID */ + NET_PACKET_GC_LOSSLESS = 0x63, /* Group chat lossless packet ID */ + NET_PACKET_GC_LOSSY = 0x64, /* Group chat lossy packet ID */ /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */ NET_PACKET_ONION_SEND_INITIAL = 0x8f, @@ -131,6 +130,17 @@ typedef enum Net_Packet_Type { NET_PACKET_ONION_RECV_2 = 0x9c, NET_PACKET_ONION_RECV_1 = 0x9d, + NET_PACKET_FORWARD_REQUEST = 0x9e, + NET_PACKET_FORWARDING = 0x9f, + NET_PACKET_FORWARD_REPLY = 0xa0, + + NET_PACKET_DATA_SEARCH_REQUEST = 0xa1, + NET_PACKET_DATA_SEARCH_RESPONSE = 0xa2, + NET_PACKET_DATA_RETRIEVE_REQUEST = 0xa3, + NET_PACKET_DATA_RETRIEVE_RESPONSE = 0xa4, + NET_PACKET_STORE_ANNOUNCE_REQUEST = 0xa5, + NET_PACKET_STORE_ANNOUNCE_RESPONSE = 0xa6, + BOOTSTRAP_INFO_PACKET_ID = 0xf1, /* Only used for bootstrap nodes */ NET_PACKET_MAX = 0xff, /* This type must remain within a single uint8. */ @@ -148,10 +158,9 @@ typedef enum Net_Packet_Type { NET_PACKET_CRYPTO = 0x20, /* Encrypted data packet ID. */ NET_PACKET_LAN_DISCOVERY = 0x21, /* LAN discovery packet ID. */ - // TODO(Jfreegman): Uncomment these when we merge the rest of new groupchats - // NET_PACKET_GC_HANDSHAKE = 0x5a, /* Group chat handshake packet ID */ - // NET_PACKET_GC_LOSSLESS = 0x5b, /* Group chat lossless packet ID */ - // NET_PACKET_GC_LOSSY = 0x5c, /* Group chat lossy packet ID */ + NET_PACKET_GC_HANDSHAKE = 0x5a, /* Group chat handshake packet ID */ + NET_PACKET_GC_LOSSLESS = 0x5b, /* Group chat lossless packet ID */ + NET_PACKET_GC_LOSSY = 0x5c, /* Group chat lossy packet ID */ /* See: `docs/Prevent_Tracking.txt` and `onion.{c,h}` */ NET_PACKET_ONION_SEND_INITIAL = 0x80, @@ -303,6 +312,8 @@ uint16_t net_htons(uint16_t hostshort); uint32_t net_ntohl(uint32_t hostlong); uint16_t net_ntohs(uint16_t hostshort); +non_null() +size_t net_pack_bool(uint8_t *bytes, bool v); non_null() size_t net_pack_u16(uint8_t *bytes, uint16_t v); non_null() @@ -310,6 +321,8 @@ size_t net_pack_u32(uint8_t *bytes, uint32_t v); non_null() size_t net_pack_u64(uint8_t *bytes, uint64_t v); +non_null() +size_t net_unpack_bool(const uint8_t *bytes, bool *v); non_null() size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v); non_null() diff --git a/toxcore/onion.c b/toxcore/onion.c index d7e3f02665..f0e4e1db4e 100644 --- a/toxcore/onion.c +++ b/toxcore/onion.c @@ -457,7 +457,8 @@ static int handle_send_2(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = plain[SIZE_IPPORT]; - if (packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && packet_id != NET_PACKET_ONION_DATA_REQUEST) { + if (packet_id != NET_PACKET_ANNOUNCE_REQUEST && packet_id != NET_PACKET_ANNOUNCE_REQUEST_OLD && + packet_id != NET_PACKET_ONION_DATA_REQUEST) { return 1; } @@ -507,7 +508,8 @@ static int handle_recv_3(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_3]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } @@ -555,7 +557,8 @@ static int handle_recv_2(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_2]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } @@ -603,7 +606,8 @@ static int handle_recv_1(void *object, const IP_Port *source, const uint8_t *pac const uint8_t packet_id = packet[1 + RETURN_1]; - if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && packet_id != NET_PACKET_ONION_DATA_RESPONSE) { + if (packet_id != NET_PACKET_ANNOUNCE_RESPONSE && packet_id != NET_PACKET_ANNOUNCE_RESPONSE_OLD && + packet_id != NET_PACKET_ONION_DATA_RESPONSE) { return 1; } diff --git a/toxcore/onion_announce.c b/toxcore/onion_announce.c index f12e560837..3bf8eba338 100644 --- a/toxcore/onion_announce.c +++ b/toxcore/onion_announce.c @@ -346,7 +346,6 @@ non_null() static int add_to_entries(Onion_Announce *onion_a, const IP_Port *ret_ip_port, const uint8_t *public_key, const uint8_t *data_public_key, const uint8_t *ret) { - int pos = in_entries(onion_a, public_key); if (pos == -1) { @@ -658,9 +657,6 @@ Onion_Announce *new_onion_announce(const Logger *log, const Random *rng, const M networking_registerhandler(onion_a->net, NET_PACKET_ANNOUNCE_REQUEST_OLD, &handle_announce_request_old, onion_a); networking_registerhandler(onion_a->net, NET_PACKET_ONION_DATA_REQUEST, &handle_data_request, onion_a); - // TODO(Jfreegman): Remove this when we merge the rest of new groupchats - onion_announce_extra_data_callback(onion_a, 0, nullptr, nullptr); - return onion_a; } diff --git a/toxcore/onion_announce.h b/toxcore/onion_announce.h index 8cf1f49539..24303abccb 100644 --- a/toxcore/onion_announce.h +++ b/toxcore/onion_announce.h @@ -20,6 +20,7 @@ #define ONION_ANNOUNCE_SENDBACK_DATA_LENGTH (sizeof(uint64_t)) +#define MAX_SENT_GC_NODES 1 #define ONION_ANNOUNCE_REQUEST_MIN_SIZE (1 + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_PING_ID_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_MAC_SIZE) #define ONION_ANNOUNCE_REQUEST_MAX_SIZE (ONION_ANNOUNCE_REQUEST_MIN_SIZE + ONION_MAX_EXTRA_DATA_SIZE) @@ -125,7 +126,7 @@ typedef int pack_extra_data_cb(void *object, const Logger *logger, const Mono_Ti uint8_t num_nodes, uint8_t *plain, uint16_t plain_size, uint8_t *response, uint16_t response_size, uint16_t offset); -non_null(1) nullable(3, 4) +non_null() void onion_announce_extra_data_callback(Onion_Announce *onion_a, uint16_t extra_data_max_size, pack_extra_data_cb *extra_data_callback, void *extra_data_object); diff --git a/toxcore/onion_client.c b/toxcore/onion_client.c index 91168c661c..ff63473e10 100644 --- a/toxcore/onion_client.c +++ b/toxcore/onion_client.c @@ -15,6 +15,7 @@ #include "LAN_discovery.h" #include "ccompat.h" +#include "group_onion_announce.h" #include "mono_time.h" #include "util.h" @@ -54,7 +55,7 @@ typedef struct Last_Pinged { uint64_t timestamp; } Last_Pinged; -typedef struct Onion_Friend { +struct Onion_Friend { bool is_valid; bool is_online; @@ -87,7 +88,12 @@ typedef struct Onion_Friend { onion_dht_pk_cb *dht_pk_callback; void *dht_pk_callback_object; uint32_t dht_pk_callback_number; -} Onion_Friend; + + uint8_t gc_data[GCA_MAX_DATA_LENGTH]; + uint8_t gc_public_key[ENC_PUBLIC_KEY_SIZE]; + uint16_t gc_data_length; + bool is_groupchat; +}; static const Onion_Friend empty_onion_friend = {false}; @@ -138,8 +144,51 @@ struct Onion_Client { unsigned int onion_connected; bool udp_connected; + + onion_group_announce_cb *group_announce_response; + void *group_announce_response_user_data; }; +uint16_t onion_get_friend_count(const Onion_Client *const onion_c) +{ + return onion_c->num_friends; +} + +Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num) +{ + return &onion_c->friends_list[friend_num]; +} + +const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend) +{ + return onion_friend->gc_public_key; +} + +const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num) +{ + return onion_c->friends_list[num].gc_public_key; +} + +void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key) +{ + memcpy(onion_friend->gc_public_key, public_key, ENC_PUBLIC_KEY_SIZE); +} + +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length) +{ + if (gc_data_length > 0 && gc_data != nullptr) { + memcpy(onion_friend->gc_data, gc_data, gc_data_length); + } + + onion_friend->gc_data_length = gc_data_length; + onion_friend->is_groupchat = true; +} + +bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend) +{ + return onion_friend->is_groupchat; +} + DHT *onion_get_dht(const Onion_Client *onion_c) { return onion_c->dht; @@ -351,8 +400,8 @@ static bool path_timed_out(const Mono_Time *mono_time, const Onion_Client_Paths const uint64_t timeout = is_new ? ONION_PATH_FIRST_TIMEOUT : ONION_PATH_TIMEOUT; return (onion_paths->last_path_used_times[pathnum] >= ONION_PATH_MAX_NO_RESPONSE_USES - && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) - || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME); + && mono_time_is_timeout(mono_time, onion_paths->last_path_used[pathnum], timeout)) + || mono_time_is_timeout(mono_time, onion_paths->path_creation_time[pathnum], ONION_PATH_MAX_LIFETIME); } /** should node be considered to have timed out */ @@ -360,8 +409,8 @@ non_null() static bool onion_node_timed_out(const Onion_Node *node, const Mono_Time *mono_time) { return node->timestamp == 0 - || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS - && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT)); + || (node->pings_since_last_response >= ONION_NODE_MAX_PINGS + && mono_time_is_timeout(mono_time, node->last_pinged, ONION_NODE_TIMEOUT)); } /** @brief Create a new path or use an old suitable one (if pathnum is valid) @@ -600,19 +649,35 @@ static int client_send_announce_request(Onion_Client *onion_c, uint32_t num, con ping_id = zero_ping_id; } - uint8_t request[ONION_ANNOUNCE_REQUEST_SIZE]; + uint8_t request[ONION_ANNOUNCE_REQUEST_MAX_SIZE]; int len; if (num == 0) { len = create_announce_request( - onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), - nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), - onion_c->temp_public_key, sendback); + onion_c->rng, request, sizeof(request), dest_pubkey, nc_get_self_public_key(onion_c->c), + nc_get_self_secret_key(onion_c->c), ping_id, nc_get_self_public_key(onion_c->c), + onion_c->temp_public_key, sendback); } else { - len = create_announce_request( - onion_c->rng, request, sizeof(request), dest_pubkey, onion_c->friends_list[num - 1].temp_public_key, - onion_c->friends_list[num - 1].temp_secret_key, ping_id, - onion_c->friends_list[num - 1].real_public_key, zero_ping_id, sendback); + Onion_Friend *onion_friend = &onion_c->friends_list[num - 1]; + + if (onion_friend->gc_data_length == 0) { // contact is a friend + len = create_announce_request( + onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, + onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, + zero_ping_id, sendback); + } else { // contact is a gc +#ifndef VANILLA_NACL + onion_friend->is_groupchat = true; + + len = create_gca_announce_request( + onion_c->rng, request, sizeof(request), dest_pubkey, onion_friend->temp_public_key, + onion_friend->temp_secret_key, ping_id, onion_friend->real_public_key, + zero_ping_id, sendback, onion_friend->gc_data, + onion_friend->gc_data_length); +#else + return -1; +#endif // VANILLA_NACL + } } if (len == -1) { @@ -852,6 +917,16 @@ static int client_ping_nodes(Onion_Client *onion_c, uint32_t num, const Node_for } non_null() +static bool handle_group_announce_response(Onion_Client *onion_c, uint32_t num, const uint8_t *plain, size_t plain_size) +{ + if (onion_c->group_announce_response == nullptr) { + return true; + } + + return onion_c->group_announce_response(onion_c, num, plain, plain_size, onion_c->group_announce_response_user_data); +} + +non_null(1, 2, 3) nullable(5) static int handle_announce_response(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, void *userdata) { @@ -861,6 +936,95 @@ static int handle_announce_response(void *object, const IP_Port *source, const u return 1; } + uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; + IP_Port ip_port; + uint32_t path_num; + const uint32_t num = check_sendback(onion_c, packet + 1, public_key, &ip_port, &path_num); + + if (num > onion_c->num_friends) { + return 1; + } + + uint8_t plain[1 + ONION_PING_ID_SIZE + ONION_ANNOUNCE_RESPONSE_MAX_SIZE - ONION_ANNOUNCE_RESPONSE_MIN_SIZE]; + const int plain_size = 1 + ONION_PING_ID_SIZE + length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE; + int len; + + if (num == 0) { + len = decrypt_data(public_key, nc_get_self_secret_key(onion_c->c), + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, + length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); + } else { + if (!onion_c->friends_list[num - 1].is_valid) { + return 1; + } + + len = decrypt_data(public_key, onion_c->friends_list[num - 1].temp_secret_key, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH, + packet + 1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE, + length - (1 + ONION_ANNOUNCE_SENDBACK_DATA_LENGTH + CRYPTO_NONCE_SIZE), plain); + } + + if ((uint32_t)len != plain_size) { + return 1; + } + + const uint32_t path_used = set_path_timeouts(onion_c, num, path_num); + + if (client_add_to_list(onion_c, num, public_key, &ip_port, plain[0], plain + 1, path_used) == -1) { + return 1; + } + + uint16_t len_nodes = 0; + const uint8_t nodes_count = plain[1 + ONION_PING_ID_SIZE]; + + if (nodes_count > 0) { + if (nodes_count > MAX_SENT_NODES) { + return 1; + } + + Node_format nodes[MAX_SENT_NODES]; + const int num_nodes = unpack_nodes(nodes, nodes_count, &len_nodes, plain + 2 + ONION_PING_ID_SIZE, + plain_size - 2 - ONION_PING_ID_SIZE, false); + + if (num_nodes < 0) { + return 1; + } + + if (client_ping_nodes(onion_c, num, nodes, num_nodes, source) == -1) { + return 1; + } + } + + if (len_nodes + 1 < length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE) { + const uint16_t offset = 2 + ONION_PING_ID_SIZE + len_nodes; + + if (plain_size < offset) { + return 1; + } + + if (!handle_group_announce_response(onion_c, num, plain + offset, plain_size - offset)) { + return 1; + } + } + + // TODO(irungentoo): LAN vs non LAN ips?, if we are connected only to LAN, are we offline? + onion_c->last_packet_recv = mono_time_get(onion_c->mono_time); + + return 0; +} + +/* TODO(jfreegman): DEPRECATE */ +non_null(1, 2, 3) nullable(5) +static int handle_announce_response_old(void *object, const IP_Port *source, const uint8_t *packet, uint16_t length, + void *userdata) +{ + Onion_Client *onion_c = (Onion_Client *)object; + + if (length < ONION_ANNOUNCE_RESPONSE_MIN_SIZE || length > ONION_ANNOUNCE_RESPONSE_MAX_SIZE) { + return 1; + } + const uint16_t len_nodes = length - ONION_ANNOUNCE_RESPONSE_MIN_SIZE; uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; @@ -1038,10 +1202,14 @@ static int handle_tcp_onion(void *object, const uint8_t *data, uint16_t length, IP_Port ip_port = {{{0}}}; ip_port.ip.family = net_family_tcp_server(); - if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { + if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE) { return handle_announce_response(object, &ip_port, data, length, userdata); } + if (data[0] == NET_PACKET_ANNOUNCE_RESPONSE_OLD) { + return handle_announce_response_old(object, &ip_port, data, length, userdata); + } + if (data[0] == NET_PACKET_ONION_DATA_RESPONSE) { return handle_data_response(object, &ip_port, data, length, userdata); } @@ -1118,8 +1286,8 @@ int send_onion_data(Onion_Client *onion_c, int friend_num, const uint8_t *data, uint8_t o_packet[ONION_MAX_PACKET_SIZE]; len = create_data_request( - onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, - node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet)); + onion_c->rng, o_packet, sizeof(o_packet), onion_c->friends_list[friend_num].real_public_key, + node_list[good_nodes[i]].data_public_key, nonce, packet, SIZEOF_VLA(packet)); if (len == -1) { continue; @@ -1167,8 +1335,8 @@ static int send_dht_dhtpk(const Onion_Client *onion_c, int friend_num, const uin uint8_t packet_data[MAX_CRYPTO_REQUEST_SIZE]; len = create_request( - onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, - onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK); + onion_c->rng, dht_get_self_public_key(onion_c->dht), dht_get_self_secret_key(onion_c->dht), packet_data, + onion_c->friends_list[friend_num].dht_public_key, temp, SIZEOF_VLA(temp), CRYPTO_PACKET_DHTPK); assert(len <= UINT16_MAX); const Packet packet = {packet_data, (uint16_t)len}; @@ -1237,7 +1405,8 @@ static int send_dhtpk_announce(Onion_Client *onion_c, uint16_t friend_num, uint8 int nodes_len = 0; if (num_nodes != 0) { - nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, nodes, num_nodes); + nodes_len = pack_nodes(onion_c->logger, data + DHTPK_DATA_MIN_LENGTH, DHTPK_DATA_MAX_LENGTH - DHTPK_DATA_MIN_LENGTH, + nodes, num_nodes); if (nodes_len <= 0) { return -1; @@ -1344,7 +1513,8 @@ int onion_addfriend(Onion_Client *onion_c, const uint8_t *public_key) onion_c->friends_list[index].is_valid = true; memcpy(onion_c->friends_list[index].real_public_key, public_key, CRYPTO_PUBLIC_KEY_SIZE); - crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, onion_c->friends_list[index].temp_secret_key); + crypto_new_keypair(onion_c->rng, onion_c->friends_list[index].temp_public_key, + onion_c->friends_list[index].temp_secret_key); return index; } @@ -1674,6 +1844,12 @@ void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_ha onion_c->onion_data_handlers[byte].object = object; } +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data) +{ + onion_c->group_announce_response = func; + onion_c->group_announce_response_user_data = user_data; +} + #define ANNOUNCE_INTERVAL_NOT_ANNOUNCED 3 #define ANNOUNCE_INTERVAL_ANNOUNCED ONION_NODE_PING_INTERVAL @@ -1925,7 +2101,8 @@ Onion_Client *new_onion_client(const Logger *logger, const Random *rng, const Mo onion_c->c = c; new_symmetric_key(rng, onion_c->secret_symmetric_key); crypto_new_keypair(rng, onion_c->temp_public_key, onion_c->temp_secret_key); - networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response, onion_c); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, &handle_announce_response, onion_c); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, &handle_announce_response_old, onion_c); networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, &handle_data_response, onion_c); oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, &handle_dhtpk_announce, onion_c); cryptopacket_registerhandler(onion_c->dht, CRYPTO_PACKET_DHTPK, &handle_dht_dhtpk, onion_c); @@ -1942,6 +2119,7 @@ void kill_onion_client(Onion_Client *onion_c) ping_array_kill(onion_c->announce_ping_array); realloc_onion_friends(onion_c, 0); + networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE, nullptr, nullptr); networking_registerhandler(onion_c->net, NET_PACKET_ANNOUNCE_RESPONSE_OLD, nullptr, nullptr); networking_registerhandler(onion_c->net, NET_PACKET_ONION_DATA_RESPONSE, nullptr, nullptr); oniondata_registerhandler(onion_c, ONION_DATA_DHTPK, nullptr, nullptr); diff --git a/toxcore/onion_client.h b/toxcore/onion_client.h index 2a30005c69..23a48ef5e6 100644 --- a/toxcore/onion_client.h +++ b/toxcore/onion_client.h @@ -43,6 +43,8 @@ #define MAX_PATH_NODES 32 +#define GCA_MAX_DATA_LENGTH GCA_PUBLIC_ANNOUNCE_MAX_SIZE + /** * If no announce response packets are received within this interval tox will * be considered offline. We give time for a node to be pinged often enough @@ -195,6 +197,13 @@ typedef int oniondata_handler_cb(void *object, const uint8_t *source_pubkey, con non_null(1) nullable(3, 4) void oniondata_registerhandler(Onion_Client *onion_c, uint8_t byte, oniondata_handler_cb *cb, void *object); +typedef bool onion_group_announce_cb(Onion_Client *onion_c, uint32_t sendback_num, const uint8_t *data, + size_t data_length, void *user_data); + +/** Function to call when the onion gets a group announce response. */ +non_null(1) nullable(2, 3) +void onion_group_announce_register(Onion_Client *onion_c, onion_group_announce_cb *func, void *user_data); + non_null() void do_onion_client(Onion_Client *onion_c); @@ -217,4 +226,15 @@ typedef enum Onion_Connection_Status { non_null() Onion_Connection_Status onion_connection_status(const Onion_Client *onion_c); +typedef struct Onion_Friend Onion_Friend; + +non_null() uint16_t onion_get_friend_count(const Onion_Client *const onion_c); +non_null() Onion_Friend *onion_get_friend(const Onion_Client *const onion_c, uint16_t friend_num); +non_null() const uint8_t *onion_friend_get_gc_public_key(const Onion_Friend *const onion_friend); +non_null() const uint8_t *onion_friend_get_gc_public_key_num(const Onion_Client *const onion_c, uint32_t num); +non_null() void onion_friend_set_gc_public_key(Onion_Friend *const onion_friend, const uint8_t *public_key); +non_null(1) nullable(2) +void onion_friend_set_gc_data(Onion_Friend *const onion_friend, const uint8_t *gc_data, uint16_t gc_data_length); +non_null() bool onion_friend_is_groupchat(const Onion_Friend *const onion_friend); + #endif diff --git a/toxcore/state.h b/toxcore/state.h index 716286d6d1..0cc188d50d 100644 --- a/toxcore/state.h +++ b/toxcore/state.h @@ -34,6 +34,7 @@ typedef enum State_Type { STATE_TYPE_NAME = 4, STATE_TYPE_STATUSMESSAGE = 5, STATE_TYPE_STATUS = 6, + STATE_TYPE_GROUPS = 7, STATE_TYPE_TCP_RELAY = 10, STATE_TYPE_PATH_NODE = 11, STATE_TYPE_CONFERENCES = 20, diff --git a/toxcore/tox.c b/toxcore/tox.c index 079f404079..852f5600b8 100644 --- a/toxcore/tox.c +++ b/toxcore/tox.c @@ -19,6 +19,8 @@ #include "Messenger.h" #include "ccompat.h" #include "group.h" +#include "group_chats.h" +#include "group_moderation.h" #include "logger.h" #include "mono_time.h" #include "network.h" @@ -54,6 +56,10 @@ static_assert(TOX_MAX_NAME_LENGTH == MAX_NAME_LENGTH, "TOX_MAX_NAME_LENGTH is assumed to be equal to MAX_NAME_LENGTH"); static_assert(TOX_MAX_STATUS_MESSAGE_LENGTH == MAX_STATUSMESSAGE_LENGTH, "TOX_MAX_STATUS_MESSAGE_LENGTH is assumed to be equal to MAX_STATUSMESSAGE_LENGTH"); +static_assert(TOX_GROUP_MAX_MESSAGE_LENGTH == GROUP_MAX_MESSAGE_LENGTH, + "TOX_GROUP_MAX_MESSAGE_LENGTH is assumed to be equal to GROUP_MAX_MESSAGE_LENGTH"); +static_assert(TOX_MAX_CUSTOM_PACKET_SIZE == MAX_GC_CUSTOM_PACKET_SIZE, + "TOX_MAX_CUSTOM_PACKET_SIZE is assumed to be equal to MAX_GC_CUSTOM_PACKET_SIZE"); struct Tox_Userdata { Tox *tox; @@ -317,8 +323,8 @@ static void tox_dht_get_nodes_response_handler(const DHT *dht, const Node_format Ip_Ntoa ip_str; tox_data->tox->dht_get_nodes_response_callback( - tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), - tox_data->user_data); + tox_data->tox, node->public_key, net_ip_ntoa(&node->ip_port.ip, &ip_str), net_ntohs(node->ip_port.port), + tox_data->user_data); } static m_friend_lossy_packet_cb tox_friend_lossy_packet_handler; @@ -353,6 +359,218 @@ static void tox_friend_lossless_packet_handler(Messenger *m, uint32_t friend_num } } +#ifndef VANILLA_NACL +non_null(1, 4) nullable(6) +static void tox_group_peer_name_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *name, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_name_callback != nullptr) { + tox_data->tox->group_peer_name_callback(tox_data->tox, group_number, peer_id, name, length, tox_data->user_data); + } +} + +non_null(1) nullable(5) +static void tox_group_peer_status_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int status, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_status_callback != nullptr) { + tox_data->tox->group_peer_status_callback(tox_data->tox, group_number, peer_id, (Tox_User_Status)status, + tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_topic_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, + size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_topic_callback != nullptr) { + tox_data->tox->group_topic_callback(tox_data->tox, group_number, peer_id, topic, length, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_topic_lock_handler(const Messenger *m, uint32_t group_number, unsigned int topic_lock, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_topic_lock_callback != nullptr) { + tox_data->tox->group_topic_lock_callback(tox_data->tox, group_number, (Tox_Group_Topic_Lock)topic_lock, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_voice_state_handler(const Messenger *m, uint32_t group_number, unsigned int voice_state, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_voice_state_callback != nullptr) { + tox_data->tox->group_voice_state_callback(tox_data->tox, group_number, (Tox_Group_Voice_State)voice_state, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_peer_limit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_limit, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_limit_callback != nullptr) { + tox_data->tox->group_peer_limit_callback(tox_data->tox, group_number, peer_limit, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_privacy_state_handler(const Messenger *m, uint32_t group_number, unsigned int privacy_state, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_privacy_state_callback != nullptr) { + tox_data->tox->group_privacy_state_callback(tox_data->tox, group_number, (Tox_Group_Privacy_State)privacy_state, + tox_data->user_data); + } +} + +non_null(1) nullable(3, 5) +static void tox_group_password_handler(const Messenger *m, uint32_t group_number, const uint8_t *password, + size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_password_callback != nullptr) { + tox_data->tox->group_password_callback(tox_data->tox, group_number, password, length, tox_data->user_data); + } +} + +non_null(1, 5) nullable(8) +static void tox_group_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, unsigned int type, + const uint8_t *message, size_t length, uint32_t message_id, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_message_callback != nullptr) { + tox_data->tox->group_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, length, + message_id, tox_data->user_data); + } +} + +non_null(1, 5) nullable(7) +static void tox_group_private_message_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int type, const uint8_t *message, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_private_message_callback != nullptr) { + tox_data->tox->group_private_message_callback(tox_data->tox, group_number, peer_id, (Tox_Message_Type)type, message, + length, + tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_custom_packet_callback != nullptr) { + tox_data->tox->group_custom_packet_callback(tox_data->tox, group_number, peer_id, data, length, tox_data->user_data); + } +} + +non_null(1, 4) nullable(6) +static void tox_group_custom_private_packet_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + const uint8_t *data, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_custom_private_packet_callback != nullptr) { + tox_data->tox->group_custom_private_packet_callback(tox_data->tox, group_number, peer_id, data, length, + tox_data->user_data); + } +} + +non_null(1, 3, 5) nullable(7) +static void tox_group_invite_handler(const Messenger *m, uint32_t friend_number, const uint8_t *invite_data, + size_t length, const uint8_t *group_name, size_t group_name_length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_invite_callback != nullptr) { + tox_data->tox->group_invite_callback(tox_data->tox, friend_number, invite_data, length, group_name, group_name_length, + tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_peer_join_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_join_callback != nullptr) { + tox_data->tox->group_peer_join_callback(tox_data->tox, group_number, peer_id, tox_data->user_data); + } +} + +non_null(1, 5) nullable(7, 9) +static void tox_group_peer_exit_handler(const Messenger *m, uint32_t group_number, uint32_t peer_id, + unsigned int exit_type, const uint8_t *name, size_t name_length, + const uint8_t *part_message, size_t length, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_peer_exit_callback != nullptr) { + tox_data->tox->group_peer_exit_callback(tox_data->tox, group_number, peer_id, (Tox_Group_Exit_Type) exit_type, name, + name_length, + part_message, length, tox_data->user_data); + } +} + +non_null(1) nullable(3) +static void tox_group_self_join_handler(const Messenger *m, uint32_t group_number, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_self_join_callback != nullptr) { + tox_data->tox->group_self_join_callback(tox_data->tox, group_number, tox_data->user_data); + } +} + +non_null(1) nullable(4) +static void tox_group_join_fail_handler(const Messenger *m, uint32_t group_number, unsigned int fail_type, + void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_join_fail_callback != nullptr) { + tox_data->tox->group_join_fail_callback(tox_data->tox, group_number, (Tox_Group_Join_Fail)fail_type, + tox_data->user_data); + } +} + +non_null(1) nullable(6) +static void tox_group_moderation_handler(const Messenger *m, uint32_t group_number, uint32_t source_peer_number, + uint32_t target_peer_number, unsigned int mod_type, void *user_data) +{ + struct Tox_Userdata *tox_data = (struct Tox_Userdata *)user_data; + + if (tox_data->tox->group_moderation_callback != nullptr) { + tox_data->tox->group_moderation_callback(tox_data->tox, group_number, source_peer_number, target_peer_number, + (Tox_Group_Mod_Event)mod_type, + tox_data->user_data); + } +} +#endif bool tox_version_is_compatible(uint32_t major, uint32_t minor, uint32_t patch) { @@ -522,6 +740,7 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) const Tox_System *sys = tox_options_get_operating_system(opts); const Tox_System default_system = tox_default_system(); + if (sys == nullptr) { sys = &default_system; } @@ -553,7 +772,8 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) const char *const proxy_host = tox_options_get_proxy_host(opts); - if (proxy_host == nullptr || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) { + if (proxy_host == nullptr + || !addr_resolve_or_parse_ip(&tox->ns, proxy_host, &m_options.proxy_info.ip_port.ip, nullptr)) { SET_ERROR_PARAMETER(error, TOX_ERR_NEW_PROXY_BAD_HOST); // TODO(irungentoo): TOX_ERR_NEW_PROXY_NOT_FOUND if domain. tox_options_free(default_options); @@ -671,6 +891,27 @@ Tox *tox_new(const struct Tox_Options *options, Tox_Err_New *error) custom_lossy_packet_registerhandler(tox->m, tox_friend_lossy_packet_handler); custom_lossless_packet_registerhandler(tox->m, tox_friend_lossless_packet_handler); +#ifndef VANILLA_NACL + m_callback_group_invite(tox->m, tox_group_invite_handler); + gc_callback_message(tox->m, tox_group_message_handler); + gc_callback_private_message(tox->m, tox_group_private_message_handler); + gc_callback_custom_packet(tox->m, tox_group_custom_packet_handler); + gc_callback_custom_private_packet(tox->m, tox_group_custom_private_packet_handler); + gc_callback_moderation(tox->m, tox_group_moderation_handler); + gc_callback_nick_change(tox->m, tox_group_peer_name_handler); + gc_callback_status_change(tox->m, tox_group_peer_status_handler); + gc_callback_topic_change(tox->m, tox_group_topic_handler); + gc_callback_peer_limit(tox->m, tox_group_peer_limit_handler); + gc_callback_privacy_state(tox->m, tox_group_privacy_state_handler); + gc_callback_topic_lock(tox->m, tox_group_topic_lock_handler); + gc_callback_password(tox->m, tox_group_password_handler); + gc_callback_peer_join(tox->m, tox_group_peer_join_handler); + gc_callback_peer_exit(tox->m, tox_group_peer_exit_handler); + gc_callback_self_join(tox->m, tox_group_self_join_handler); + gc_callback_rejected(tox->m, tox_group_join_fail_handler); + gc_callback_voice_state(tox->m, tox_group_voice_state_handler); +#endif + tox_options_free(default_options); tox_unlock(tox); @@ -714,9 +955,9 @@ size_t tox_get_savedata_size(const Tox *tox) assert(tox != nullptr); tox_lock(tox); const size_t ret = 2 * sizeof(uint32_t) - + messenger_size(tox->m) - + conferences_size(tox->m->conferences_object) - + end_size(); + + messenger_size(tox->m) + + conferences_size(tox->m->conferences_object) + + end_size(); tox_unlock(tox); return ret; } @@ -749,7 +990,8 @@ void tox_get_savedata(const Tox *tox, uint8_t *savedata) } non_null(5) nullable(1, 2, 4, 6) -static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, IP_Port **root, Tox_Err_Bootstrap *error) +static int32_t resolve_bootstrap_node(Tox *tox, const char *host, uint16_t port, const uint8_t *public_key, + IP_Port **root, Tox_Err_Bootstrap *error) { assert(tox != nullptr); assert(root != nullptr); @@ -2580,3 +2822,1760 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error) tox_unlock(tox); return 0; } + +/* GROUPCHAT FUNCTIONS */ + +#ifndef VANILLA_NACL +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback) +{ + assert(tox != nullptr); + tox->group_invite_callback = callback; +} + +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback) +{ + assert(tox != nullptr); + tox->group_message_callback = callback; +} + +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback) +{ + assert(tox != nullptr); + tox->group_private_message_callback = callback; +} + +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback) +{ + assert(tox != nullptr); + tox->group_custom_packet_callback = callback; +} + +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback) +{ + assert(tox != nullptr); + tox->group_custom_private_packet_callback = callback; +} + +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback) +{ + assert(tox != nullptr); + tox->group_moderation_callback = callback; +} + +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_name_callback = callback; +} + +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_status_callback = callback; +} + +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback) +{ + assert(tox != nullptr); + tox->group_topic_callback = callback; +} + +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback) +{ + assert(tox != nullptr); + tox->group_privacy_state_callback = callback; +} + +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback) +{ + assert(tox != nullptr); + tox->group_topic_lock_callback = callback; +} + +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback) +{ + assert(tox != nullptr); + tox->group_voice_state_callback = callback; +} + +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_limit_callback = callback; +} + +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback) +{ + assert(tox != nullptr); + tox->group_password_callback = callback; +} + +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_join_callback = callback; +} + +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback) +{ + assert(tox != nullptr); + tox->group_peer_exit_callback = callback; +} + +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback) +{ + assert(tox != nullptr); + tox->group_self_join_callback = callback; +} + +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback) +{ + assert(tox != nullptr); + tox->group_join_fail_callback = callback; +} + +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, + size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_group_add(tox->m->group_handler, (Group_Privacy_State) privacy_state, + group_name, group_name_length, name, name_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_TOO_LONG); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_EMPTY); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_INIT); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_STATE); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_NEW_ANNOUNCE); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, + const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_group_join(tox->m->group_handler, chat_id, name, name_length, password, password_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_INIT); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_BAD_CHAT_ID); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_TOO_LONG); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_EMPTY); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_PASSWORD); + return UINT32_MAX; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_JOIN_CORE); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_IS_CONNECTED_OK); + + const bool ret = chat->connection_state == CS_CONNECTED; + tox_unlock(tox); + + return ret; +} + +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED); + tox_unlock(tox); + return false; + } + + + const bool ret = gc_disconnect_from_group(tox->m->group_handler, chat); + + tox_unlock(tox); + + if (!ret) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_DISCONNECT_OK); + + return true; +} + +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_rejoin_group(tox->m->group_handler, chat); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_RECONNECT_CORE); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, + Tox_Err_Group_Leave *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_group_exit(tox->m->group_handler, chat, part_message, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_LEAVE_FAIL_SEND); + return true; /* the group was still successfully deleted */ + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, + Tox_Err_Group_Self_Name_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_self_nick(tox->m, group_number, name, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_INVALID); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const size_t ret = gc_get_self_nick_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + gc_get_self_nick(chat, name); + tox_unlock(tox); + + return true; +} + +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, + Tox_Err_Group_Self_Status_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_self_status(tox->m, group_number, (Group_Peer_Status) status); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_User_Status) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const uint8_t status = gc_get_self_status(chat); + tox_unlock(tox); + + return (Tox_User_Status)status; +} + +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Role) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const Group_Role role = gc_get_self_role(chat); + tox_unlock(tox); + + return (Tox_Group_Role)role; +} + +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + const uint32_t ret = gc_get_self_peer_id(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, + Tox_Err_Group_Self_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SELF_QUERY_OK); + + gc_get_self_public_key(chat, public_key); + tox_unlock(tox); + + return true; +} + +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + const int ret = gc_get_peer_nick_size(chat, peer_id); + tox_unlock(tox); + + if (ret == -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return -1; + } else { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return ret; + } +} + +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const bool ret = gc_get_peer_nick(chat, peer_id, name); + tox_unlock(tox); + + if (!ret) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return true; +} + +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_User_Status) - 1; + } + + const uint8_t ret = gc_get_status(chat, peer_id); + tox_unlock(tox); + + if (ret == UINT8_MAX) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return (Tox_User_Status) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_User_Status)ret; +} + +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Role) - 1; + } + + const uint8_t ret = gc_get_role(chat, peer_id); + tox_unlock(tox); + + if (ret == (uint8_t) -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return (Tox_Group_Role) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_Group_Role)ret; +} + +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_get_peer_public_key_by_peer_id(chat, peer_id, public_key); + tox_unlock(tox); + + if (ret == -1) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return true; +} + +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND); + tox_unlock(tox); + return TOX_CONNECTION_NONE; + } + + const unsigned int ret = gc_get_peer_connection_status(chat, peer_id); + tox_unlock(tox); + + if (ret == 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND); + return TOX_CONNECTION_NONE; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_PEER_QUERY_OK); + return (Tox_Connection)ret; +} + +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, + Tox_Err_Group_Topic_Set *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_set_topic(chat, topic, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_topic_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + gc_get_topic(chat, topic); + tox_unlock(tox); + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + return true; +} + +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_group_name_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + gc_get_group_name(chat, group_name); + tox_unlock(tox); + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + return true; +} + +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + gc_get_chat_id(chat, chat_id); + tox_unlock(tox); + + return true; +} + +uint32_t tox_group_get_number_groups(const Tox *tox) +{ + assert(tox != nullptr); + + tox_lock(tox); + const uint32_t ret = gc_count_groups(tox->m->group_handler); + tox_unlock(tox); + + return ret; +} + +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Privacy_State) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const uint8_t state = gc_get_privacy_state(chat); + tox_unlock(tox); + + return (Tox_Group_Privacy_State)state; +} + +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Topic_Lock) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const Group_Topic_Lock topic_lock = gc_get_topic_lock_state(chat); + tox_unlock(tox); + + return (Tox_Group_Topic_Lock)topic_lock; +} + +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return (Tox_Group_Voice_State) - 1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const Group_Voice_State voice_state = gc_get_voice_state(chat); + tox_unlock(tox); + + return (Tox_Group_Voice_State)voice_state; +} + +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const uint16_t ret = gc_get_max_peers(chat); + tox_unlock(tox); + + return ret; +} + +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return -1; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + const size_t ret = gc_get_password_size(chat); + tox_unlock(tox); + + return ret; +} + +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, + Tox_Err_Group_State_Queries *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_STATE_QUERIES_OK); + + gc_get_password(chat, password); + tox_unlock(tox); + + return true; +} + +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, + size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_message(chat, message, length, type, message_id); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_private_message(chat, peer_id, type, message, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, + size_t length, Tox_Err_Group_Send_Custom_Packet *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_custom_packet(chat, lossless, data, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, + const uint8_t *data, size_t length, + Tox_Err_Group_Send_Custom_Private_Packet *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_send_custom_private_packet(chat, lossless, peer_id, data, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, + Tox_Err_Group_Invite_Friend *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED); + tox_unlock(tox); + return false; + } + + if (!friend_is_valid(tox->m, friend_number)) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_invite_friend(tox->m->group_handler, chat, friend_number, send_group_invite_packet); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *name, size_t name_length, const uint8_t *password, + size_t password_length, Tox_Err_Group_Invite_Accept *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_accept_invite(tox->m->group_handler, friend_number, invite_data, length, name, name_length, password, + password_length); + tox_unlock(tox); + + if (ret >= 0) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_OK); + return ret; + } + + switch (ret) { + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE); + return UINT32_MAX; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED); + return UINT32_MAX; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG); + return UINT32_MAX; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY); + return UINT32_MAX; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD); + return UINT32_MAX; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_CORE); + return UINT32_MAX; + } + + case -7: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND); + return UINT32_MAX; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return UINT32_MAX; +} + +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + Tox_Err_Group_Founder_Set_Password *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_founder_set_password(chat, password, length); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + Tox_Err_Group_Founder_Set_Privacy_State *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_privacy_state(tox->m, group_number, (Group_Privacy_State) privacy_state); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, + Tox_Err_Group_Founder_Set_Topic_Lock *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_topic_lock(tox->m, group_number, (Group_Topic_Lock) topic_lock); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + Tox_Err_Group_Founder_Set_Voice_State *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_founder_set_voice_state(tox->m, group_number, (Group_Voice_State)voice_state); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, + Tox_Err_Group_Founder_Set_Peer_Limit *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + if (chat->connection_state == CS_DISCONNECTED) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED); + tox_unlock(tox); + return false; + } + + const int ret = gc_founder_set_max_peers(chat, max_peers); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, + Tox_Err_Group_Set_Ignore *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const GC_Chat *chat = gc_get_group(tox->m->group_handler, group_number); + + if (chat == nullptr) { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND); + tox_unlock(tox); + return false; + } + + const int ret = gc_set_ignore(chat, peer_id, ignore); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_SET_IGNORE_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, + Tox_Err_Group_Mod_Set_Role *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_set_peer_role(tox->m, group_number, peer_id, (Group_Role) role); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_SET_ROLE_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Mod_Kick_Peer *error) +{ + assert(tox != nullptr); + + tox_lock(tox); + const int ret = gc_kick_peer(tox->m, group_number, peer_id); + tox_unlock(tox); + + switch (ret) { + case 0: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_OK); + return true; + } + + case -1: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND); + return false; + } + + case -2: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND); + return false; + } + + case -3: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS); + return false; + } + + case -4: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION); + return false; + } + + case -5: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND); + return false; + } + + case -6: { + SET_ERROR_PARAMETER(error, TOX_ERR_GROUP_MOD_KICK_PEER_SELF); + return false; + } + } + + /* can't happen */ + LOGGER_FATAL(tox->m->log, "impossible return value: %d", ret); + + return false; +} + +#endif /* VANILLA_NACL */ + diff --git a/toxcore/tox.h b/toxcore/tox.h index e8d55c6689..125de83b3a 100644 --- a/toxcore/tox.h +++ b/toxcore/tox.h @@ -3267,6 +3267,2171 @@ uint16_t tox_self_get_tcp_port(const Tox *tox, Tox_Err_Get_Port *error); /** @} */ +/******************************************************************************* + * + * :: Group chats + * + ******************************************************************************/ + + + + +/******************************************************************************* + * + * :: Group chat numeric constants + * + ******************************************************************************/ + + + +/** @{ + * Maximum length of a group topic. + */ +#define TOX_GROUP_MAX_TOPIC_LENGTH 512 + +uint32_t tox_group_max_topic_length(void); + +/** + * Maximum length of a peer part message. + */ +#define TOX_GROUP_MAX_PART_LENGTH 128 + +uint32_t tox_group_max_part_length(void); + +/** + * Maximum length of a group text message. + */ +#define TOX_GROUP_MAX_MESSAGE_LENGTH 1368 + +/** + * Maximum length of a group name. + */ +#define TOX_GROUP_MAX_GROUP_NAME_LENGTH 48 + +uint32_t tox_group_max_group_name_length(void); + +/** + * Maximum length of a group password. + */ +#define TOX_GROUP_MAX_PASSWORD_SIZE 32 + +uint32_t tox_group_max_password_size(void); + +/** + * Number of bytes in a group Chat ID. + */ +#define TOX_GROUP_CHAT_ID_SIZE 32 + +uint32_t tox_group_chat_id_size(void); + +/** + * Size of a peer public key. + */ +#define TOX_GROUP_PEER_PUBLIC_KEY_SIZE 32 + +uint32_t tox_group_peer_public_key_size(void); + + +/******************************************************************************* + * + * :: Group chat state enumerators + * + ******************************************************************************/ + + + +/** + * Represents the group privacy state. + */ +typedef enum Tox_Group_Privacy_State { + + /** + * The group is considered to be public. Anyone may join the group using the Chat ID. + * + * If the group is in this state, even if the Chat ID is never explicitly shared + * with someone outside of the group, information including the Chat ID, IP addresses, + * and peer ID's (but not Tox ID's) is visible to anyone with access to a node + * storing a DHT entry for the given group. + */ + TOX_GROUP_PRIVACY_STATE_PUBLIC, + + /** + * The group is considered to be private. The only way to join the group is by having + * someone in your contact list send you an invite. + * + * If the group is in this state, no group information (mentioned above) is present in the DHT; + * the DHT is not used for any purpose at all. If a public group is set to private, + * all DHT information related to the group will expire shortly. + */ + TOX_GROUP_PRIVACY_STATE_PRIVATE, + +} Tox_Group_Privacy_State; + + +/** + * Represents the state of the group topic lock. + */ +typedef enum Tox_Group_Topic_Lock { + + /** + * The topic lock is enabled. Only peers with the founder and moderator roles may set the topic. + */ + TOX_GROUP_TOPIC_LOCK_ENABLED, + + /** + * The topic lock is disabled. All peers except those with the observer role may set the topic. + */ + TOX_GROUP_TOPIC_LOCK_DISABLED, + +} Tox_Group_Topic_Lock; + +/** + * Represents the group voice state, which determines which Group Roles have permission to speak + * in the group chat. The voice state does not have any effect private messages or topic setting. + */ +typedef enum Tox_Group_Voice_State { + /** + * All group roles above Observer have permission to speak. + */ + TOX_GROUP_VOICE_STATE_ALL, + + /** + * Moderators and Founders have permission to speak. + */ + TOX_GROUP_VOICE_STATE_MODERATOR, + + /** + * Only the founder may speak. + */ + TOX_GROUP_VOICE_STATE_FOUNDER, +} Tox_Group_Voice_State; + +/** + * Represents group roles. + * + * Roles are hierarchical in that each role has a set of privileges plus all the privileges + * of the roles below it. + */ +typedef enum Tox_Group_Role { + + /** + * May kick all other peers as well as set their role to anything (except founder). + * Founders may also set the group password, toggle the privacy state, and set the peer limit. + */ + TOX_GROUP_ROLE_FOUNDER, + + /** + * May kick and set the user and observer roles for peers below this role. + * May also set the group topic. + */ + TOX_GROUP_ROLE_MODERATOR, + + /** + * May communicate with other peers normally. + */ + TOX_GROUP_ROLE_USER, + + /** + * May observe the group and ignore peers; may not communicate with other peers or with the group. + */ + TOX_GROUP_ROLE_OBSERVER, + +} Tox_Group_Role; + + + +/******************************************************************************* + * + * :: Group chat instance management + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_New { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_NEW_OK, + + /** + * name exceeds TOX_MAX_NAME_LENGTH or group_name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH. + */ + TOX_ERR_GROUP_NEW_TOO_LONG, + + /** + * name or group_name is NULL or length is zero. + */ + TOX_ERR_GROUP_NEW_EMPTY, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_NEW_INIT, + + /** + * The group state failed to initialize. This usually indicates that something went wrong + * related to cryptographic signing. + */ + TOX_ERR_GROUP_NEW_STATE, + + /** + * The group failed to announce to the DHT. This indicates a network related error. + */ + TOX_ERR_GROUP_NEW_ANNOUNCE, + +} Tox_Err_Group_New; + + +/** + * Creates a new group chat. + * + * This function creates a new group chat object and adds it to the chats array. + * + * The caller of this function has Founder role privileges. + * + * The client should initiate its peer list with self info after calling this function, as + * the peer_join callback will not be triggered. + * + * @param privacy_state The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC, + * the group will attempt to announce itself to the DHT and anyone with the Chat ID may join. + * Otherwise a friend invite will be required to join the group. + * @param group_name The name of the group. The name must be non-NULL. + * @param group_name_length The length of the group name. This must be greater than zero and no larger than + * TOX_GROUP_MAX_GROUP_NAME_LENGTH. + * @param name The name of the peer creating the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_new(Tox *tox, Tox_Group_Privacy_State privacy_state, const uint8_t *group_name, + size_t group_name_length, const uint8_t *name, size_t name_length, Tox_Err_Group_New *error); + +typedef enum Tox_Err_Group_Join { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_JOIN_OK, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_JOIN_INIT, + + /** + * The chat_id pointer is set to NULL or a group with chat_id already exists. This usually + * happens if the client attempts to create multiple sessions for the same group. + */ + TOX_ERR_GROUP_JOIN_BAD_CHAT_ID, + + /** + * name is NULL or name_length is zero. + */ + TOX_ERR_GROUP_JOIN_EMPTY, + + /** + * name exceeds TOX_MAX_NAME_LENGTH. + */ + TOX_ERR_GROUP_JOIN_TOO_LONG, + + /** + * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_JOIN_PASSWORD, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_JOIN_CORE, + +} Tox_Err_Group_Join; + + +/** + * Joins a group chat with specified Chat ID. + * + * This function creates a new group chat object, adds it to the chats array, and sends + * a DHT announcement to find peers in the group associated with chat_id. Once a peer has been + * found a join attempt will be initiated. + * + * @param chat_id The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If length is equal to zero, + * the password parameter is ignored. length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * + * @return group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_join(Tox *tox, const uint8_t *chat_id, const uint8_t *name, size_t name_length, + const uint8_t *password, size_t password_length, Tox_Err_Group_Join *error); + +typedef enum Tox_Err_Group_Is_Connected { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_IS_CONNECTED_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND, + +} Tox_Err_Group_Is_Connected; + + +/** + * Returns true if the group chat is currently connected or attempting to connect to other peers + * in the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_is_connected(const Tox *tox, uint32_t group_number, Tox_Err_Group_Is_Connected *error); + +typedef enum Tox_Err_Group_Disconnect { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_DISCONNECT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND, + + /** + * The group is already disconnected. + */ + TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED, +} Tox_Err_Group_Disconnect; + + +/** + * Disconnects from a group chat while retaining the group state and credentials. + * + * Returns true if we successfully disconnect from the group. + * + * @param group_number The group number of the designated group. + */ +bool tox_group_disconnect(const Tox *tox, uint32_t group_number, Tox_Err_Group_Disconnect *error); + +typedef enum Tox_Err_Group_Reconnect { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_RECONNECT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_RECONNECT_CORE, + +} Tox_Err_Group_Reconnect; + + +/** + * Reconnects to a group. + * + * This function disconnects from all peers in the group, then attempts to reconnect with the group. + * The caller's state is not changed (i.e. name, status, role, chat public key etc.). + * + * @param group_number The group number of the group we wish to reconnect to. + * + * @return true on success. + */ +bool tox_group_reconnect(Tox *tox, uint32_t group_number, Tox_Err_Group_Reconnect *error); + +typedef enum Tox_Err_Group_Leave { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_LEAVE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_PART_LENGTH. + */ + TOX_ERR_GROUP_LEAVE_TOO_LONG, + + /** + * The parting packet failed to send. + */ + TOX_ERR_GROUP_LEAVE_FAIL_SEND, +} Tox_Err_Group_Leave; + + +/** + * Leaves a group. + * + * This function sends a parting packet containing a custom (non-obligatory) message to all + * peers in a group, and deletes the group from the chat array. All group state information is permanently + * lost, including keys and role credentials. + * + * @param group_number The group number of the group we wish to leave. + * @param part_message The parting message to be sent to all the peers. Set to NULL if we do not wish to + * send a parting message. + * @param length The length of the parting message. Set to 0 if we do not wish to send a parting message. + * + * @return true if the group chat instance is successfully deleted. + */ +bool tox_group_leave(Tox *tox, uint32_t group_number, const uint8_t *part_message, size_t length, + Tox_Err_Group_Leave *error); + + +/******************************************************************************* + * + * :: Group user-visible client information (nickname/status/role/public key) + * + ******************************************************************************/ + + + +/** + * General error codes for self state get and size functions. + */ +typedef enum Tox_Err_Group_Self_Query { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_QUERY_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND, + +} Tox_Err_Group_Self_Query; + + +/** + * Error codes for self name setting. + */ +typedef enum Tox_Err_Group_Self_Name_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_NAME_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND, + + /** + * Name length exceeded TOX_MAX_NAME_LENGTH. + */ + TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG, + + /** + * The length given to the set function is zero or name is a NULL pointer. + */ + TOX_ERR_GROUP_SELF_NAME_SET_INVALID, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Name_Set; + + +/** + * Set the client's nickname for the group instance designated by the given group number. + * + * Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is equal to zero or name is a NULL + * pointer, the function call will fail. + * + * @param name A byte array containing the new nickname. + * @param length The size of the name byte array. + * + * @return true on success. + */ +bool tox_group_self_set_name(const Tox *tox, uint32_t group_number, const uint8_t *name, size_t length, + Tox_Err_Group_Self_Name_Set *error); + +/** + * Return the length of the client's current nickname for the group instance designated + * by group_number as passed to tox_group_self_set_name. + * + * If no nickname was set before calling this function, the name is empty, + * and this function returns 0. + * + * @see threading for concurrency implications. + */ +size_t tox_group_self_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the nickname set by tox_group_self_set_name to a byte array. + * + * If no nickname was set before calling this function, the name is empty, + * and this function has no effect. + * + * Call tox_group_self_get_name_size to find out how much memory to allocate for the result. + * + * @param name A valid memory location large enough to hold the nickname. + * If this parameter is NULL, the function has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_name(const Tox *tox, uint32_t group_number, uint8_t *name, Tox_Err_Group_Self_Query *error); + +/** + * Error codes for self status setting. + */ +typedef enum Tox_Err_Group_Self_Status_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND, + +} Tox_Err_Group_Self_Status_Set; + + +/** + * Set the client's status for the group instance. Status must be a Tox_User_Status. + * + * @return true on success. + */ +bool tox_group_self_set_status(const Tox *tox, uint32_t group_number, Tox_User_Status status, + Tox_Err_Group_Self_Status_Set *error); + +/** + * returns the client's status for the group instance on success. + * return value is unspecified on failure. + */ +Tox_User_Status tox_group_self_get_status(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's role for the group instance on success. + * return value is unspecified on failure. + */ +Tox_Group_Role tox_group_self_get_role(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * returns the client's peer id for the group instance on success. + * return value is unspecified on failure. + */ +uint32_t tox_group_self_get_peer_id(const Tox *tox, uint32_t group_number, Tox_Err_Group_Self_Query *error); + +/** + * Write the client's group public key designated by the given group number to a byte array. + * + * This key will be permanently tied to the client's identity for this particular group until + * the client explicitly leaves the group. This key is the only way for other peers to reliably + * identify the client across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. + * + * @param public_key A valid memory region large enough to store the public key. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_self_get_public_key(const Tox *tox, uint32_t group_number, uint8_t *public_key, + Tox_Err_Group_Self_Query *error); + + +/******************************************************************************* + * + * :: Peer-specific group state queries. + * + ******************************************************************************/ + + + +/** + * Error codes for peer info queries. + */ +typedef enum Tox_Err_Group_Peer_Query { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_PEER_QUERY_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND, + +} Tox_Err_Group_Peer_Query; + + +/** + * Return the length of the peer's name. If the group number or ID is invalid, the + * return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name length we want to retrieve. + * + * The return value is equal to the `length` argument received by the last + * `group_peer_name` callback. + */ +size_t tox_group_peer_get_name_size(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Write the name of the peer designated by the given ID to a byte + * array. + * + * Call tox_group_peer_get_name_size to determine the allocation size for the `name` parameter. + * + * The data written to `name` is equal to the data received by the last + * `group_peer_name` callback. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose name we wish to retrieve. + * @param name A valid memory region large enough to store the friend's name. + * + * @return true on success. + */ +bool tox_group_peer_get_name(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *name, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's user status (away/busy/...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose status we wish to query. + * + * The status returned is equal to the last status received through the + * `group_peer_status` callback. + */ +Tox_User_Status tox_group_peer_get_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the peer's role (user/moderator/founder...). If the ID or group number is + * invalid, the return value is unspecified. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose role we wish to query. + * + * The role returned is equal to the last role received through the + * `group_moderation` callback. + */ +Tox_Group_Role tox_group_peer_get_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Return the type of connection we have established with a peer. + * + * This function will return an error if called on ourselves. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose connection status we wish to query. + */ +Tox_Connection tox_group_peer_get_connection_status(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Peer_Query *error); + +/** + * Write the group public key with the designated peer_id for the designated group number to public_key. + * + * This key will be permanently tied to a particular peer until they explicitly leave the group or + * get kicked, and is the only way to reliably identify the same peer across client restarts. + * + * `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes. If `public_key` is null + * this function has no effect. + * + * @param group_number The group number of the group we wish to query. + * @param peer_id The ID of the peer whose public key we wish to retrieve. + * @param public_key A valid memory region large enough to store the public key. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_peer_get_public_key(const Tox *tox, uint32_t group_number, uint32_t peer_id, uint8_t *public_key, + Tox_Err_Group_Peer_Query *error); + +/** + * @param group_number The group number of the group the name change is intended for. + * @param peer_id The ID of the peer who has changed their name. + * @param name The name data. + * @param length The length of the name. + */ +typedef void tox_group_peer_name_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *name, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_peer_name` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their nickname. + */ +void tox_callback_group_peer_name(Tox *tox, tox_group_peer_name_cb *callback); + +/** + * @param group_number The group number of the group the status change is intended for. + * @param peer_id The ID of the peer who has changed their status. + * @param status The new status of the peer. + */ +typedef void tox_group_peer_status_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_User_Status status, + void *user_data); + + +/** + * Set the callback for the `group_peer_status` event. Pass NULL to unset. + * + * This event is triggered when a peer changes their status. + */ +void tox_callback_group_peer_status(Tox *tox, tox_group_peer_status_cb *callback); + + +/******************************************************************************* + * + * :: Group chat state queries and events. + * + ******************************************************************************/ + + + +/** + * General error codes for group state get and size functions. + */ +typedef enum Tox_Err_Group_State_Queries { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_STATE_QUERIES_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND, + +} Tox_Err_Group_State_Queries; + + +/** + * Error codes for group topic setting. + */ +typedef enum Tox_Err_Group_Topic_Set { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_TOPIC_SET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND, + + /** + * Topic length exceeded TOX_GROUP_MAX_TOPIC_LENGTH. + */ + TOX_ERR_GROUP_TOPIC_SET_TOO_LONG, + + /** + * The caller does not have the required permissions to set the topic. + */ + TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS, + + /** + * The packet could not be created. This error is usually related to cryptographic signing. + */ + TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_TOPIC_SET_DISCONNECTED, + +} Tox_Err_Group_Topic_Set; + + +/** + * Set the group topic and broadcast it to the rest of the group. + * + * topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH. If length is equal to zero or + * topic is set to NULL, the topic will be unset. + * + * @return true on success. + */ +bool tox_group_set_topic(const Tox *tox, uint32_t group_number, const uint8_t *topic, size_t length, + Tox_Err_Group_Topic_Set *error); + +/** + * Return the length of the group topic. If the group number is invalid, the + * return value is unspecified. + * + * The return value is equal to the `length` argument received by the last + * `group_topic` callback. + */ +size_t tox_group_get_topic_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the topic designated by the given group number to a byte array. + * + * Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter. + * + * The data written to `topic` is equal to the data received by the last + * `group_topic` callback. + * + * @param topic A valid memory region large enough to store the topic. + * If this parameter is NULL, this function has no effect. + * + * @return true on success. + */ +bool tox_group_get_topic(const Tox *tox, uint32_t group_number, uint8_t *topic, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the topic change is intended for. + * @param peer_id The ID of the peer who changed the topic. If the peer who set the topic + * is not present in our peer list this value will be set to 0. + * @param topic The topic data. + * @param length The topic length. + */ +typedef void tox_group_topic_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *topic, size_t length, + void *user_data); + + +/** + * Set the callback for the `group_topic` event. Pass NULL to unset. + * + * This event is triggered when a peer changes the group topic. + */ +void tox_callback_group_topic(Tox *tox, tox_group_topic_cb *callback); + +/** + * Return the length of the group name. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_name_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the name of the group designated by the given group number to a byte array. + * + * Call tox_group_get_name_size to determine the allocation size for the `name` parameter. + * + * @param group_name A valid memory region large enough to store the group name. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_name(const Tox *tox, uint32_t group_number, uint8_t *group_name, Tox_Err_Group_State_Queries *error); + +/** + * Write the Chat ID designated by the given group number to a byte array. + * + * `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes. + * + * @param chat_id A valid memory region large enough to store the Chat ID. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_chat_id(const Tox *tox, uint32_t group_number, uint8_t *chat_id, Tox_Err_Group_State_Queries *error); + +/** + * Return the number of groups in the Tox chats array. + */ +uint32_t tox_group_get_number_groups(const Tox *tox); + +/** + * Return the privacy state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_privacy_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Privacy_State tox_group_get_privacy_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the privacy state is intended for. + * @param privacy_state The new privacy state. + */ +typedef void tox_group_privacy_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the privacy state. + */ +void tox_callback_group_privacy_state(Tox *tox, tox_group_privacy_state_cb *callback); + +/** + * Return the voice state of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last `group_voice_state` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +Tox_Group_Voice_State tox_group_get_voice_state(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group the voice state change is intended for. + * @param voice_state The new voice state. + */ +typedef void tox_group_voice_state_cb(Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + void *user_data); + + +/** + * Set the callback for the `group_privacy_state` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the voice state. + */ +void tox_callback_group_voice_state(Tox *tox, tox_group_voice_state_cb *callback); + +/** + * Return the topic lock status of the group designated by the given group number. If group number + * is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_topic_lock` callback. + * + * @see the `Group chat founder contols` section for the respective set function. + */ +Tox_Group_Topic_Lock tox_group_get_topic_lock(const Tox *tox, uint32_t group_number, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the topic lock has changed. + * @param topic_lock The new topic lock state. + */ +typedef void tox_group_topic_lock_cb(Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, void *user_data); + + + +/** + * Set the callback for the `group_topic_lock` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the topic lock status. + */ +void tox_callback_group_topic_lock(Tox *tox, tox_group_topic_lock_cb *callback); + +/** + * Return the maximum number of peers allowed for the group designated by the given group number. + * If the group number is invalid, the return value is unspecified. + * + * The value returned is equal to the data received by the last + * `group_peer_limit` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + */ +uint16_t tox_group_get_peer_limit(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the peer limit has changed. + * @param peer_limit The new peer limit for the group. + */ +typedef void tox_group_peer_limit_cb(Tox *tox, uint32_t group_number, uint32_t peer_limit, void *user_data); + + +/** + * Set the callback for the `group_peer_limit` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the maximum peer limit. + */ +void tox_callback_group_peer_limit(Tox *tox, tox_group_peer_limit_cb *callback); + +/** + * Return the length of the group password. If the group number is invalid, the + * return value is unspecified. + */ +size_t tox_group_get_password_size(const Tox *tox, uint32_t group_number, Tox_Err_Group_State_Queries *error); + +/** + * Write the password for the group designated by the given group number to a byte array. + * + * Call tox_group_get_password_size to determine the allocation size for the `password` parameter. + * + * The data received is equal to the data received by the last + * `group_password` callback. + * + * @see the `Group chat founder controls` section for the respective set function. + * + * @param password A valid memory region large enough to store the group password. + * If this parameter is NULL, this function call has no effect. + * + * @return true on success. + */ +bool tox_group_get_password(const Tox *tox, uint32_t group_number, uint8_t *password, + Tox_Err_Group_State_Queries *error); + +/** + * @param group_number The group number of the group for which the password has changed. + * @param password The new group password. + * @param length The length of the password. + */ +typedef void tox_group_password_cb(Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + void *user_data); + + +/** + * Set the callback for the `group_password` event. Pass NULL to unset. + * + * This event is triggered when the group founder changes the group password. + */ +void tox_callback_group_password(Tox *tox, tox_group_password_cb *callback); + + +/******************************************************************************* + * + * :: Group chat message sending + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Send_Message { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_MESSAGE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. + */ + TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_MESSAGE_EMPTY, + + /** + * The message type is invalid. + */ + TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_MESSAGE_DISCONNECTED, + +} Tox_Err_Group_Send_Message; + + +/** + * Send a text chat message to the group. + * + * This function creates a group message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param type Message type (normal, action, ...). + * @param message A non-NULL pointer to the first element of a byte array + * containing the message text. + * @param length Length of the message to be sent. + * @param message_id A pointer to a uint32_t. The message_id of this message will be returned + * unless the parameter is NULL, in which case the returned parameter value will be undefined. + * If this function returns false the returned parameter `message_id` value will also be undefined. + * + * @return true on success. + */ +bool tox_group_send_message(const Tox *tox, uint32_t group_number, Tox_Message_Type type, const uint8_t *message, + size_t length, uint32_t *message_id, Tox_Err_Group_Send_Message *error); + +typedef enum Tox_Err_Group_Send_Private_Message { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND, + + /** + * The peer ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_DISCONNECTED, + + /** + * The message type is invalid. + */ + TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_BAD_TYPE, + +} Tox_Err_Group_Send_Private_Message; + + +/** + * Send a text chat message to the specified peer in the specified group. + * + * This function creates a group private message packet and pushes it into the send + * queue. + * + * The message length may not exceed TOX_GROUP_MAX_MESSAGE_LENGTH. Larger messages + * must be split by the client and sent as separate messages. Other clients can + * then reassemble the fragments. Messages may not be empty. + * + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer the message is intended for. + * @param message A non-NULL pointer to the first element of a byte array + * containing the message text. + * @param length Length of the message to be sent. + * + * @return true on success. + */ +bool tox_group_send_private_message(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, Tox_Err_Group_Send_Private_Message *error); + +typedef enum Tox_Err_Group_Send_Custom_Packet { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_GROUP_MAX_MESSAGE_LENGTH. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Packet; + + +/** + * Send a custom packet to the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The message length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_packet(const Tox *tox, uint32_t group_number, bool lossless, const uint8_t *data, + size_t length, + Tox_Err_Group_Send_Custom_Packet *error); + + +typedef enum Tox_Err_Group_Send_Custom_Private_Packet { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_GROUP_NOT_FOUND, + + /** + * Message length exceeded TOX_MAX_CUSTOM_PACKET_SIZE. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_TOO_LONG, + + /** + * The message pointer is null or length is zero. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_EMPTY, + + /** + * The peer ID passed did no designate a valid peer. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions to send group messages. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_PERMISSIONS, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_SEND_CUSTOM_PRIVATE_PACKET_DISCONNECTED, + +} Tox_Err_Group_Send_Custom_Private_Packet; + +/** + * Send a custom private packet to a designated peer in the group. + * + * If lossless is true the packet will be lossless. Lossless packet behaviour is comparable + * to TCP (reliability, arrive in order) but with packets instead of a stream. + * + * If lossless is false, the packet will be lossy. Lossy packets behave like UDP packets, + * meaning they might never reach the other side or might arrive more than once (if someone + * is messing with the connection) or might arrive in the wrong order. + * + * Unless latency is an issue or message reliability is not important, it is recommended that you use + * lossless packets. + * + * The packet length may not exceed TOX_MAX_CUSTOM_PACKET_SIZE. Larger packets + * must be split by the client and sent as separate packets. Other clients can + * then reassemble the fragments. Packets may not be empty. + * + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer the packet is intended for. + * @param lossless True if the packet should be lossless. + * @param data A byte array containing the packet data. + * @param length The length of the packet data byte array. + * + * @return true on success. + */ +bool tox_group_send_custom_private_packet(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool lossless, + const uint8_t *data, size_t length, + Tox_Err_Group_Send_Custom_Private_Packet *error); + + +/******************************************************************************* + * + * :: Group chat message receiving + * + ******************************************************************************/ + + + +/** + * @param group_number The group number of the group the message is intended for. + * @param peer_id The ID of the peer who sent the message. + * @param type The type of message (normal, action, ...). + * @param message The message data. + * @param message_id A pseudo message id that clients can use to uniquely identify this group message. + * @param length The length of the message. + */ +typedef void tox_group_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, uint32_t message_id, void *user_data); + + +/** + * Set the callback for the `group_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group message. + */ +void tox_callback_group_message(Tox *tox, tox_group_message_cb *callback); + +/** + * @param group_number The group number of the group the private message is intended for. + * @param peer_id The ID of the peer who sent the private message. + * @param message The message data. + * @param length The length of the message. + */ +typedef void tox_group_private_message_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Message_Type type, + const uint8_t *message, size_t length, void *user_data); + + +/** + * Set the callback for the `group_private_message` event. Pass NULL to unset. + * + * This event is triggered when the client receives a private message. + */ +void tox_callback_group_private_message(Tox *tox, tox_group_private_message_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom packet. + */ +void tox_callback_group_custom_packet(Tox *tox, tox_group_custom_packet_cb *callback); + +/** + * @param group_number The group number of the group the packet is intended for. + * @param peer_id The ID of the peer who sent the packet. + * @param data The packet data. + * @param length The length of the data. + */ +typedef void tox_group_custom_private_packet_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, const uint8_t *data, + size_t length, void *user_data); + + +/** + * Set the callback for the `group_custom_private_packet` event. Pass NULL to unset. + * + * This event is triggered when the client receives a custom private packet. + */ +void tox_callback_group_custom_private_packet(Tox *tox, tox_group_custom_private_packet_cb *callback); + + +/******************************************************************************* + * + * :: Group chat inviting and join/part events + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Invite_Friend { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_INVITE_FRIEND_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND, + + /** + * The friend number passed did not designate a valid friend. + */ + TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND, + + /** + * Creation of the invite packet failed. This indicates a network related error. + */ + TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_INVITE_FRIEND_DISCONNECTED, + +} Tox_Err_Group_Invite_Friend; + + +/** + * Invite a friend to a group. + * + * This function creates an invite request packet and pushes it to the send queue. + * + * @param group_number The group number of the group the message is intended for. + * @param friend_number The friend number of the friend the invite is intended for. + * + * @return true on success. + */ +bool tox_group_invite_friend(const Tox *tox, uint32_t group_number, uint32_t friend_number, + Tox_Err_Group_Invite_Friend *error); + +typedef enum Tox_Err_Group_Invite_Accept { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_OK, + + /** + * The invite data is not in the expected format. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE, + + /** + * The group instance failed to initialize. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED, + + /** + * name exceeds TOX_MAX_NAME_LENGTH + */ + TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG, + + /** + * name is NULL or name_length is zero. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_EMPTY, + + /** + * Failed to set password. This usually occurs if the password exceeds TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_PASSWORD, + + /** + * There was a core error when initiating the group. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_CORE, + + /** + * Packet failed to send. + */ + TOX_ERR_GROUP_INVITE_ACCEPT_FAIL_SEND, + +} Tox_Err_Group_Invite_Accept; + + +/** + * Accept an invite to a group chat that the client previously received from a friend. The invite + * is only valid while the inviter is present in the group. + * + * @param invite_data The invite data received from the `group_invite` event. + * @param length The length of the invite data. + * @param name The name of the peer joining the group. + * @param name_length The length of the peer's name. This must be greater than zero and no larger + * than TOX_MAX_NAME_LENGTH. + * @param password The password required to join the group. Set to NULL if no password is required. + * @param password_length The length of the password. If password_length is equal to zero, the password + * parameter will be ignored. password_length must be no larger than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return the group_number on success, UINT32_MAX on failure. + */ +uint32_t tox_group_invite_accept(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *name, size_t name_length, const uint8_t *password, size_t password_length, + Tox_Err_Group_Invite_Accept *error); + +/** + * @param friend_number The friend number of the contact who sent the invite. + * @param invite_data The invite data. + * @param length The length of invite_data. + */ +typedef void tox_group_invite_cb(Tox *tox, uint32_t friend_number, const uint8_t *invite_data, size_t length, + const uint8_t *group_name, size_t group_name_length, void *user_data); + + +/** + * Set the callback for the `group_invite` event. Pass NULL to unset. + * + * This event is triggered when the client receives a group invite from a friend. The client must store + * invite_data which is used to join the group via tox_group_invite_accept. + */ +void tox_callback_group_invite(Tox *tox, tox_group_invite_cb *callback); + +/** + * @param group_number The group number of the group in which a new peer has joined. + * @param peer_id The permanent ID of the new peer. This id should not be relied on for + * client behaviour and should be treated as a random value. + */ +typedef void tox_group_peer_join_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, void *user_data); + + +/** + * Set the callback for the `group_peer_join` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self joins the group. + */ +void tox_callback_group_peer_join(Tox *tox, tox_group_peer_join_cb *callback); + +/** + * Represents peer exit events. These should be used with the `group_peer_exit` event. + */ +typedef enum Tox_Group_Exit_Type { + + /** + * The peer has quit the group. + */ + TOX_GROUP_EXIT_TYPE_QUIT, + + /** + * Your connection with this peer has timed out. + */ + TOX_GROUP_EXIT_TYPE_TIMEOUT, + + /** + * Your connection with this peer has been severed. + */ + TOX_GROUP_EXIT_TYPE_DISCONNECTED, + + /** + * Your connection with all peers has been severed. This will occur when you are kicked from + * a group, rejoin a group, or manually disconnect from a group. + */ + TOX_GROUP_EXIT_TYPE_SELF_DISCONNECTED, + + /** + * The peer has been kicked. + */ + TOX_GROUP_EXIT_TYPE_KICK, + + /** + * The peer provided invalid group sync information. + */ + TOX_GROUP_EXIT_TYPE_SYNC_ERROR, + +} Tox_Group_Exit_Type; + + +/** + * @param group_number The group number of the group in which a peer has left. + * @param peer_id The ID of the peer who left the group. This ID no longer designates a valid peer + * and cannot be used for API calls. + * @param exit_type The type of exit event. One of Tox_Group_Exit_Type. + * @param name The nickname of the peer who left the group. + * @param part_message The parting message data. + * @param length The length of the parting message. + */ +typedef void tox_group_peer_exit_cb(Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Exit_Type exit_type, + const uint8_t *name, size_t name_length, const uint8_t *part_message, size_t length, void *user_data); + + +/** + * Set the callback for the `group_peer_exit` event. Pass NULL to unset. + * + * This event is triggered when a peer other than self exits the group. + */ +void tox_callback_group_peer_exit(Tox *tox, tox_group_peer_exit_cb *callback); + +/** + * @param group_number The group number of the group that the client has joined. + */ +typedef void tox_group_self_join_cb(Tox *tox, uint32_t group_number, void *user_data); + + +/** + * Set the callback for the `group_self_join` event. Pass NULL to unset. + * + * This event is triggered when the client has successfully joined a group. Use this to initialize + * any group information the client may need. + */ +void tox_callback_group_self_join(Tox *tox, tox_group_self_join_cb *callback); + +/** + * Represents types of failed group join attempts. These are used in the tox_callback_group_rejected + * callback when a peer fails to join a group. + */ +typedef enum Tox_Group_Join_Fail { + + /** + * The group peer limit has been reached. + */ + TOX_GROUP_JOIN_FAIL_PEER_LIMIT, + + /** + * You have supplied an invalid password. + */ + TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD, + + /** + * The join attempt failed due to an unspecified error. This often occurs when the group is + * not found in the DHT. + */ + TOX_GROUP_JOIN_FAIL_UNKNOWN, + +} Tox_Group_Join_Fail; + + +/** + * @param group_number The group number of the group for which the join has failed. + * @param fail_type The type of group rejection. + */ +typedef void tox_group_join_fail_cb(Tox *tox, uint32_t group_number, Tox_Group_Join_Fail fail_type, void *user_data); + + +/** + * Set the callback for the `group_join_fail` event. Pass NULL to unset. + * + * This event is triggered when the client fails to join a group. + */ +void tox_callback_group_join_fail(Tox *tox, tox_group_join_fail_cb *callback); + + +/******************************************************************************* + * + * :: Group chat founder controls (these only work for the group founder) + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Founder_Set_Password { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the password. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS, + + /** + * Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND, + + /** + * The function failed to allocate enough memory for the operation. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_MALLOC, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Password; + + +/** + * Set or unset the group password. + * + * This function sets the groups password, creates a new group shared state including the change, + * and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the password. + * @param password The password we want to set. Set password to NULL to unset the password. + * @param length The length of the password. length must be no longer than TOX_GROUP_MAX_PASSWORD_SIZE. + * + * @return true on success. + */ +bool tox_group_founder_set_password(const Tox *tox, uint32_t group_number, const uint8_t *password, size_t length, + Tox_Err_Group_Founder_Set_Password *error); + +typedef enum Tox_Err_Group_Founder_Set_Topic_Lock { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_GROUP_NOT_FOUND, + + /** + * Tox_Group_Topic_Lock is an invalid type. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_INVALID, + + /** + * The caller does not have the required permissions to set the topic lock. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_PERMISSIONS, + + /** + * The topic lock could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_TOPIC_LOCK_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Topic_Lock; + + +/** + * Set the group topic lock state. + * + * This function sets the group's topic lock state to enabled or disabled, creates a new shared + * state including the change, and distributes it to the rest of the group. + * + * When the topic lock is enabled, only the group founder and moderators may set the topic. + * When disabled, all peers except those with the observer role may set the topic. + * + * @param group_number The group number of the group for which we wish to change the topic lock state. + * @param topic_lock The state we wish to set the topic lock to. + * + * @return true on success. + */ +bool tox_group_founder_set_topic_lock(const Tox *tox, uint32_t group_number, Tox_Group_Topic_Lock topic_lock, + Tox_Err_Group_Founder_Set_Topic_Lock *error); + +typedef enum Tox_Err_Group_Founder_Set_Voice_State { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the privacy state. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_PERMISSIONS, + + /** + * The voice state could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_VOICE_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Voice_State; + +/** + * Set the group voice state. + * + * This function sets the group's voice state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the voice state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the voice state. + * @param voice_state The voice state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_voice_state(const Tox *tox, uint32_t group_number, Tox_Group_Voice_State voice_state, + Tox_Err_Group_Founder_Set_Voice_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Privacy_State { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the privacy state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS, + + /** + * The privacy state could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Privacy_State; + +/** + * Set the group privacy state. + * + * This function sets the group's privacy state, creates a new group shared state + * including the change, and distributes it to the rest of the group. + * + * If an attempt is made to set the privacy state to the same state that the group is already + * in, the function call will be successful and no action will be taken. + * + * @param group_number The group number of the group for which we wish to change the privacy state. + * @param privacy_state The privacy state we wish to set the group to. + * + * @return true on success. + */ +bool tox_group_founder_set_privacy_state(const Tox *tox, uint32_t group_number, Tox_Group_Privacy_State privacy_state, + Tox_Err_Group_Founder_Set_Privacy_State *error); + +typedef enum Tox_Err_Group_Founder_Set_Peer_Limit { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND, + + /** + * The caller does not have the required permissions to set the peer limit. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS, + + /** + * The peer limit could not be set. This may occur due to an error related to + * cryptographic signing of the new shared state. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND, + + /** + * The group is disconnected. + */ + TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_DISCONNECTED, + +} Tox_Err_Group_Founder_Set_Peer_Limit; + + +/** + * Set the group peer limit. + * + * This function sets a limit for the number of peers who may be in the group, creates a new + * group shared state including the change, and distributes it to the rest of the group. + * + * @param group_number The group number of the group for which we wish to set the peer limit. + * @param max_peers The maximum number of peers to allow in the group. + * + * @return true on success. + */ +bool tox_group_founder_set_peer_limit(const Tox *tox, uint32_t group_number, uint16_t max_peers, + Tox_Err_Group_Founder_Set_Peer_Limit *error); + + +/******************************************************************************* + * + * :: Group chat moderation + * + ******************************************************************************/ + + + +typedef enum Tox_Err_Group_Set_Ignore { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_SET_IGNORE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_SET_IGNORE_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_SET_IGNORE_PEER_NOT_FOUND, + + /** + * The caller attempted to ignore himself. + */ + TOX_ERR_GROUP_SET_IGNORE_SELF, + +} Tox_Err_Group_Set_Ignore; + + +/** + * Ignore or unignore a peer. + * + * @param group_number The group number of the group in which you wish to ignore a peer. + * @param peer_id The ID of the peer who shall be ignored or unignored. + * @param ignore True to ignore the peer, false to unignore the peer. + * + * @return true on success. + */ +bool tox_group_set_ignore(const Tox *tox, uint32_t group_number, uint32_t peer_id, bool ignore, + Tox_Err_Group_Set_Ignore *error); + +typedef enum Tox_Err_Group_Mod_Set_Role { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. Note: you cannot set your own role. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions for this action. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS, + + /** + * The role assignment is invalid. This will occur if you try to set a peer's role to + * the role they already have. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT, + + /** + * The role was not successfully set. This may occur if the packet failed to send, or + * if the role limit has been reached. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION, + + /** + * The caller attempted to set their own role. + */ + TOX_ERR_GROUP_MOD_SET_ROLE_SELF, + +} Tox_Err_Group_Mod_Set_Role; + + +/** + * Set a peer's role. + * + * This function will first remove the peer's previous role and then assign them a new role. + * It will also send a packet to the rest of the group, requesting that they perform + * the role reassignment. Note: peers cannot be set to the founder role. + * + * @param group_number The group number of the group the in which you wish set the peer's role. + * @param peer_id The ID of the peer whose role you wish to set. + * @param role The role you wish to set the peer to. + * + * @return true on success. + */ +bool tox_group_mod_set_role(const Tox *tox, uint32_t group_number, uint32_t peer_id, Tox_Group_Role role, + Tox_Err_Group_Mod_Set_Role *error); + +typedef enum Tox_Err_Group_Mod_Kick_Peer { + + /** + * The function returned successfully. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_OK, + + /** + * The group number passed did not designate a valid group. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_GROUP_NOT_FOUND, + + /** + * The ID passed did not designate a valid peer. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_PEER_NOT_FOUND, + + /** + * The caller does not have the required permissions for this action. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_PERMISSIONS, + + /** + * The peer could not be kicked from the group. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_ACTION, + + /** + * The packet failed to send. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_FAIL_SEND, + + /** + * The caller attempted to set their own role. + */ + TOX_ERR_GROUP_MOD_KICK_PEER_SELF, + +} Tox_Err_Group_Mod_Kick_Peer; + + +/** + * Kick a peer. + * + * This function will remove a peer from the caller's peer list and send a packet to all + * group members requesting them to do the same. Note: This function will not trigger + * the `group_peer_exit` event for the caller. + * + * @param group_number The group number of the group the action is intended for. + * @param peer_id The ID of the peer who will be kicked. + * + * @return true on success. + */ +bool tox_group_mod_kick_peer(const Tox *tox, uint32_t group_number, uint32_t peer_id, + Tox_Err_Group_Mod_Kick_Peer *error); + +/** + * Represents moderation events. These should be used with the `group_moderation` event. + */ +typedef enum Tox_Group_Mod_Event { + + /** + * A peer has been kicked from the group. + */ + TOX_GROUP_MOD_EVENT_KICK, + + /** + * A peer as been given the observer role. + */ + TOX_GROUP_MOD_EVENT_OBSERVER, + + /** + * A peer has been given the user role. + */ + TOX_GROUP_MOD_EVENT_USER, + + /** + * A peer has been given the moderator role. + */ + TOX_GROUP_MOD_EVENT_MODERATOR, + +} Tox_Group_Mod_Event; + + +/** + * @param group_number The group number of the group the event is intended for. + * @param source_peer_id The ID of the peer who initiated the event. + * @param target_peer_id The ID of the peer who is the target of the event. + * @param mod_type The type of event. + */ +typedef void tox_group_moderation_cb(Tox *tox, uint32_t group_number, uint32_t source_peer_id, uint32_t target_peer_id, + Tox_Group_Mod_Event mod_type, void *user_data); + + +/** + * Set the callback for the `group_moderation` event. Pass NULL to unset. + * + * This event is triggered when a moderator or founder executes a moderation event, with + * the exception of the peer who initiates the event. It is also triggered when the + * observer and moderator lists are silently modified (this may occur during group syncing). + * + * If either peer id does not designate a valid peer in the group chat, the client should + * manually update all peer roles. + */ +void tox_callback_group_moderation(Tox *tox, tox_group_moderation_cb *callback); + +/** @} */ + /** @} */ #ifdef __cplusplus diff --git a/toxcore/tox_dispatch.c b/toxcore/tox_dispatch.c index 4b4546e4a4..5427851470 100644 --- a/toxcore/tox_dispatch.c +++ b/toxcore/tox_dispatch.c @@ -47,9 +47,11 @@ Tox_Dispatch *tox_dispatch_new(Tox_Err_Dispatch_New *error) *dispatch = (Tox_Dispatch) { nullptr }; + if (error != nullptr) { *error = TOX_ERR_DISPATCH_NEW_OK; } + return dispatch; } diff --git a/toxcore/tox_struct.h b/toxcore/tox_struct.h index 22d1c54a27..8b95d83bbc 100644 --- a/toxcore/tox_struct.h +++ b/toxcore/tox_struct.h @@ -44,6 +44,24 @@ struct Tox { tox_dht_get_nodes_response_cb *dht_get_nodes_response_callback; tox_friend_lossy_packet_cb *friend_lossy_packet_callback_per_pktid[UINT8_MAX + 1]; tox_friend_lossless_packet_cb *friend_lossless_packet_callback_per_pktid[UINT8_MAX + 1]; + tox_group_peer_name_cb *group_peer_name_callback; + tox_group_peer_status_cb *group_peer_status_callback; + tox_group_topic_cb *group_topic_callback; + tox_group_privacy_state_cb *group_privacy_state_callback; + tox_group_topic_lock_cb *group_topic_lock_callback; + tox_group_voice_state_cb *group_voice_state_callback; + tox_group_peer_limit_cb *group_peer_limit_callback; + tox_group_password_cb *group_password_callback; + tox_group_message_cb *group_message_callback; + tox_group_private_message_cb *group_private_message_callback; + tox_group_custom_packet_cb *group_custom_packet_callback; + tox_group_custom_private_packet_cb *group_custom_private_packet_callback; + tox_group_invite_cb *group_invite_callback; + tox_group_peer_join_cb *group_peer_join_callback; + tox_group_peer_exit_cb *group_peer_exit_callback; + tox_group_self_join_cb *group_self_join_callback; + tox_group_join_fail_cb *group_join_fail_callback; + tox_group_moderation_cb *group_moderation_callback; void *toxav_object; // workaround to store a ToxAV object (setter and getter functions are available) }; diff --git a/toxcore/util.c b/toxcore/util.c index c70c3a76f5..402977a680 100644 --- a/toxcore/util.c +++ b/toxcore/util.c @@ -139,12 +139,12 @@ uint32_t jenkins_one_at_a_time_hash(const uint8_t *key, size_t len) for (uint32_t i = 0; i < len; ++i) { hash += key[i]; - hash += hash << 10; + hash += (uint32_t)((uint64_t)hash << 10); hash ^= hash >> 6; } - hash += hash << 3; + hash += (uint32_t)((uint64_t)hash << 3); hash ^= hash >> 11; - hash += hash << 15; + hash += (uint32_t)((uint64_t)hash << 15); return hash; }