From 82c98d2bc3d87534c8638ef7bb64e0b7df2aa4c1 Mon Sep 17 00:00:00 2001
From: Andrea Gianarda <andrea.gianarda@belledonne-communications.com>
Date: Tue, 25 Feb 2025 17:22:36 +0100
Subject: [PATCH] Set the From header of MESSAGE requests to the account
 identity address for basic chatroom. Flexisip based chatroom will set it to
 the contact address of the account. As RFC3428 doesn't allow MESSAGE requests
 to have a Contact header, the account contact address is the only way for the
 server to know which device sent it.

---
 src/account/account-params.cpp                |   2 +-
 src/account/account-params.h                  |   2 +-
 src/chat/chat-message/chat-message.cpp        |  51 ++++---
 src/chat/chat-room/abstract-chat-room.h       |   2 +-
 .../modifier/cpim-chat-message-modifier.cpp   |  31 ++++-
 tester/group_chat_tester.c                    | 124 +++++++++++++++++-
 6 files changed, 187 insertions(+), 25 deletions(-)

diff --git a/src/account/account-params.cpp b/src/account/account-params.cpp
index 51c21e753f..6b602b2e19 100644
--- a/src/account/account-params.cpp
+++ b/src/account/account-params.cpp
@@ -949,7 +949,7 @@ LinphonePrivacyMask AccountParams::getPrivacy() const {
 	return mPrivacy;
 }
 
-std::shared_ptr<const Address> AccountParams::getIdentityAddress() const {
+std::shared_ptr<Address> AccountParams::getIdentityAddress() const {
 	return mIdentityAddress;
 }
 
diff --git a/src/account/account-params.h b/src/account/account-params.h
index b34453d5aa..fb14d4bdd5 100644
--- a/src/account/account-params.h
+++ b/src/account/account-params.h
@@ -145,7 +145,7 @@ public:
 	const std::list<std::string> getRoutesString() const;
 	const bctbx_list_t *getRoutesCString() const;
 	LinphonePrivacyMask getPrivacy() const;
-	std::shared_ptr<const Address> getIdentityAddress() const;
+	std::shared_ptr<Address> getIdentityAddress() const;
 	LinphoneAVPFMode getAvpfMode() const;
 	std::shared_ptr<NatPolicy> getNatPolicy() const;
 	PushNotificationConfig *getPushNotificationConfig() const;
diff --git a/src/chat/chat-message/chat-message.cpp b/src/chat/chat-message/chat-message.cpp
index 36b002711c..dd7b2c500f 100644
--- a/src/chat/chat-message/chat-message.cpp
+++ b/src/chat/chat-message/chat-message.cpp
@@ -800,28 +800,49 @@ std::string ChatMessagePrivate::createFakeFileTransferFromUrl(const std::string
 }
 
 void ChatMessagePrivate::setChatRoom(const shared_ptr<AbstractChatRoom> &chatRoom) {
+	L_Q();
 	mChatRoom = chatRoom;
 	const ConferenceId &conferenceId = chatRoom->getConferenceId();
 	const auto &account = chatRoom->getAccount();
-	// If an account is attached to a chatroom, use its contact address otherwise use the local address of the
-	// conference ID. Note that the conference ID's local address may not have the "gr" parameter hence this may lead to
-	// issues such as IMDN not received
+	const bool isBasicChatRoom =
+	    (chatRoom->getCurrentParams()->getChatParams()->getBackend() == ChatParams::Backend::Basic);
+	// If an account is attached to a chatroom, use its contact or the identity address otherwise use the local address
+	// of the conference ID. For Flexisip based chatrooms, it is paramount to use a contact address as the From header.
+	// RFC3428 forbids adding a Contact header to MESSAGE requests, therefore the From header is the only way a
+	// conference server knows which device sent the request and can be forwarded to the other tchat members as well as
+	// other devices of the same participant.
 	std::shared_ptr<Address> localAddress = nullptr;
-	if (account && account->getContactAddress()) {
-		localAddress = account->getContactAddress()->clone()->toSharedPtr();
-	} else {
-		lInfo() << "It looks that chatroom " << chatRoom << " with ID " << conferenceId
-		        << " has no account associated to or the contact address is not available yet, setting conference ID's "
-		           "local address as message local address";
-		localAddress = conferenceId.getLocalAddress()->clone()->toSharedPtr();
+	if (account) {
+		if (isBasicChatRoom) {
+			localAddress = account->getAccountParams()->getIdentityAddress();
+			lInfo() << *chatRoom << " with ID " << conferenceId
+			        << " is a basic chatroom therefore set the local address of message [" << q
+			        << "] to the account identity address " << *localAddress;
+		} else {
+			localAddress = account->getContactAddress();
+			if (localAddress) {
+				lInfo() << *chatRoom << " with ID " << conferenceId
+				        << " is not a basic chatroom therefore set the local address of message [" << q
+				        << "] to the account contact address " << *localAddress;
+			}
+		}
+	}
+	if (!localAddress) {
+		lInfo() << *chatRoom << " with ID " << conferenceId
+		        << " has no account associated to or the contact or the identity address is not available yet, setting "
+		           "conference ID's local address as message local address";
+		localAddress = conferenceId.getLocalAddress();
 	}
 
-	auto peerAddress = conferenceId.getPeerAddress()->clone()->toSharedPtr();
-	const bool isBasicChatRoom =
-	    (chatRoom->getCurrentParams()->getChatParams()->getBackend() == ChatParams::Backend::Basic);
-	if (isBasicChatRoom && localAddress) {
-		localAddress = Address::create(localAddress->getUriWithoutGruu());
+	if (localAddress) {
+		if (isBasicChatRoom) {
+			localAddress = Address::create(localAddress->getUriWithoutGruu());
+		} else {
+			localAddress = localAddress->clone()->toSharedPtr();
+		}
 	}
+
+	auto peerAddress = conferenceId.getPeerAddress()->clone()->toSharedPtr();
 	if (direction == ChatMessage::Direction::Outgoing) {
 		fromAddress = localAddress;
 		toAddress = peerAddress;
diff --git a/src/chat/chat-room/abstract-chat-room.h b/src/chat/chat-room/abstract-chat-room.h
index 2e762b32b2..dda0d01ddf 100644
--- a/src/chat/chat-room/abstract-chat-room.h
+++ b/src/chat/chat-room/abstract-chat-room.h
@@ -287,7 +287,7 @@ std::ostream &operator<<(std::ostream &lhs, AbstractChatRoom::EphemeralMode e);
 
 inline std::ostream &operator<<(std::ostream &str, const AbstractChatRoom &chatRoom) {
 	const auto &conferenceAddress = chatRoom.getConferenceAddress();
-	str << "Conference [" << &chatRoom << "] ("
+	str << "ChatRoom [" << &chatRoom << "] ("
 	    << (conferenceAddress ? conferenceAddress->toString() : std::string("sip:")) << ")";
 	return str;
 }
diff --git a/src/chat/modifier/cpim-chat-message-modifier.cpp b/src/chat/modifier/cpim-chat-message-modifier.cpp
index d135445de0..88d21220d7 100644
--- a/src/chat/modifier/cpim-chat-message-modifier.cpp
+++ b/src/chat/modifier/cpim-chat-message-modifier.cpp
@@ -59,11 +59,29 @@ const string imdnDispositionNotificationHeader = "Disposition-Notification";
 ChatMessageModifier::Result CpimChatMessageModifier::encode(const shared_ptr<ChatMessage> &message,
                                                             BCTBX_UNUSED(int &errorCode)) {
 	Cpim::Message cpimMessage;
-
-	cpimMessage.addMessageHeader(
-	    Cpim::FromHeader(cpimAddressUri(message->getFromAddress()), cpimAddressDisplayName(message->getFromAddress())));
-	cpimMessage.addMessageHeader(
-	    Cpim::ToHeader(cpimAddressUri(message->getToAddress()), cpimAddressDisplayName(message->getToAddress())));
+	shared_ptr<AbstractChatRoom> chatRoom = message->getChatRoom();
+	const auto &account = chatRoom->getAccount();
+	if (!account) {
+		lWarning() << "Unable to encode CPIM because the account attached to the message is unknown";
+		return ChatMessageModifier::Result::Error;
+	}
+	const bool isBasicChatRoom =
+	    (chatRoom->getCurrentParams()->getChatParams()->getBackend() == ChatParams::Backend::Basic);
+	std::shared_ptr<Address> localDevice;
+	// For non-basic tchat, prefer using the account contact address in case the message from address is the account's
+	// identity address. It will help the server to correctly dispatch requests to the other participants and sender
+	// devices
+	if (!isBasicChatRoom) {
+		localDevice = account->getContactAddress();
+	}
+	if (!localDevice) {
+		localDevice = message->getFromAddress();
+	}
+	localDevice = localDevice->clone()->toSharedPtr();
+	localDevice->setDisplayName("");
+	cpimMessage.addMessageHeader(Cpim::FromHeader(cpimAddressUri(localDevice), cpimAddressDisplayName(localDevice)));
+	const auto &to = message->getToAddress();
+	cpimMessage.addMessageHeader(Cpim::ToHeader(cpimAddressUri(to), cpimAddressDisplayName(to)));
 	cpimMessage.addMessageHeader(Cpim::DateTimeHeader(message->getTime()));
 
 	bool linphoneNamespaceHeaderSet = false;
@@ -313,7 +331,8 @@ CpimChatMessageModifier::createMinimalCpimContentForLimeMessage(const shared_ptr
 	const auto &localDevice =
 	    accountContactAddress ? accountContactAddress : chatRoom->getConferenceId().getLocalAddress();
 	if (!localDevice) {
-		lWarning() << "Unable to create CPIM because account [" << account << "] has not registered yet and the chatroom local address is unknown";
+		lWarning() << "Unable to create CPIM because " << *account
+		           << " has not registered yet and the chatroom local address is unknown";
 		return Content::create();
 	}
 	Cpim::Message cpimMessage;
diff --git a/tester/group_chat_tester.c b/tester/group_chat_tester.c
index 3f40af1945..74bb37d691 100644
--- a/tester/group_chat_tester.c
+++ b/tester/group_chat_tester.c
@@ -1021,7 +1021,7 @@ static void group_chat_room_creation_core_restart(void) {
 		uuid = bctbx_strdup(linphone_config_get_string(linphone_core_get_config(marie->lc), "misc", "uuid", NULL));
 	}
 
-	ms_message("Restart %s'c core", linphone_core_get_identity(marie->lc));
+	ms_message("Restart %s's core", linphone_core_get_identity(marie->lc));
 	initialMarieStats = marie->stat;
 	coresList = bctbx_list_remove(coresList, marie->lc);
 	linphone_core_manager_reinit(marie);
@@ -6187,6 +6187,126 @@ end:
 	linphone_core_manager_destroy(chloe);
 }
 
+static void basic_chat_room_with_cpim_base(bool_t use_gruu) {
+	LinphoneCoreManager *marie = linphone_core_manager_create("marie_rc");
+	LinphoneCoreManager *pauline = linphone_core_manager_create("pauline_rc");
+	bctbx_list_t *coresManagerList = NULL;
+	coresManagerList = bctbx_list_append(coresManagerList, marie);
+	coresManagerList = bctbx_list_append(coresManagerList, pauline);
+	if (!use_gruu) {
+		linphone_core_remove_supported_tag(marie->lc, "gruu");
+	}
+	bctbx_list_t *coresList = init_core_for_conference(coresManagerList);
+	start_core_for_conference(coresManagerList);
+
+	// Enable CPIM
+	LinphoneAccount *marie_account = linphone_core_get_default_account(marie->lc);
+	const LinphoneAccountParams *marie_account_params = linphone_account_get_params(marie_account);
+	LinphoneAccountParams *cloned_marie_account_params = linphone_account_params_clone(marie_account_params);
+	linphone_account_params_enable_cpim_in_basic_chat_room(cloned_marie_account_params, TRUE);
+	linphone_account_set_params(marie_account, cloned_marie_account_params);
+	linphone_account_params_unref(cloned_marie_account_params);
+
+	LinphoneAccount *pauline_account = linphone_core_get_default_account(pauline->lc);
+	const LinphoneAccountParams *pauline_account_params = linphone_account_get_params(pauline_account);
+	LinphoneAccountParams *cloned_pauline_account_params = linphone_account_params_clone(pauline_account_params);
+	linphone_account_params_enable_cpim_in_basic_chat_room(cloned_pauline_account_params, TRUE);
+	linphone_account_set_params(pauline_account, cloned_pauline_account_params);
+	linphone_account_params_unref(cloned_pauline_account_params);
+
+	LinphoneAddress *pauline_contact_address = linphone_account_get_contact_address(pauline_account);
+
+	bctbx_list_t *participantsAddresses = NULL;
+	participantsAddresses = bctbx_list_append(participantsAddresses, pauline_contact_address);
+	LinphoneConferenceParams *marie_params = linphone_core_create_conference_params(marie->lc);
+	linphone_conference_params_enable_chat(marie_params, TRUE);
+	linphone_conference_params_enable_group(marie_params, FALSE);
+	LinphoneChatParams *marie_chat_params = linphone_conference_params_get_chat_params(marie_params);
+	linphone_chat_params_set_backend(marie_chat_params, LinphoneChatRoomBackendBasic);
+	LinphoneChatRoom *marieCr =
+	    linphone_core_create_chat_room_7(marie->lc, marie_params, marie->identity, participantsAddresses);
+	bctbx_list_free(participantsAddresses);
+	participantsAddresses = NULL;
+	linphone_chat_room_params_unref(marie_params);
+	BC_ASSERT_PTR_NOT_NULL(marieCr);
+	BC_ASSERT_TRUE(wait_for_list(coresList, &marie->stat.number_of_LinphoneChatRoomStateCreated, 1,
+	                             liblinphone_tester_sip_timeout));
+
+	// Marie sends a message in a basic chatroom
+	const char *marie_text = "This is a basic chat room";
+	if (marieCr) {
+		const LinphoneAddress *peer_address = linphone_chat_room_get_peer_address(marieCr);
+		BC_ASSERT_TRUE(linphone_address_has_uri_param(peer_address, "gr"));
+		LinphoneChatMessage *sent_msg = _send_message(marieCr, marie_text);
+		BC_ASSERT_TRUE(
+		    wait_for_list(coresList, &marie->stat.number_of_LinphoneMessageSent, 1, liblinphone_tester_sip_timeout));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &pauline->stat.number_of_LinphoneChatRoomStateCreated, 1,
+		                             liblinphone_tester_sip_timeout));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &pauline->stat.number_of_LinphoneMessageReceived, 1,
+		                             liblinphone_tester_sip_timeout));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &marie->stat.number_of_LinphoneMessageDelivered, 1,
+		                             liblinphone_tester_sip_timeout));
+		linphone_chat_message_unref(sent_msg);
+	}
+
+	LinphoneConferenceParams *pauline_params = linphone_core_create_conference_params(pauline->lc);
+	linphone_conference_params_enable_chat(pauline_params, TRUE);
+	linphone_conference_params_enable_group(pauline_params, FALSE);
+	LinphoneChatParams *pauline_chat_params = linphone_conference_params_get_chat_params(pauline_params);
+	linphone_chat_params_set_backend(pauline_chat_params, LinphoneChatRoomBackendBasic);
+	const LinphoneAddress *pauline_identity_address =
+	    linphone_account_params_get_identity_address(linphone_account_get_params(pauline_account));
+	LinphoneChatRoom *paulineCr =
+	    linphone_core_search_chat_room_2(pauline->lc, pauline_params, pauline_identity_address, NULL, NULL);
+	BC_ASSERT_PTR_NOT_NULL(paulineCr);
+	linphone_chat_room_params_unref(pauline_params);
+
+	const char *pauline_text = "Yes Madam";
+	if (paulineCr) {
+		LinphoneChatMessage *msg = linphone_chat_room_get_last_message_in_history(paulineCr);
+		if (BC_ASSERT_PTR_NOT_NULL(msg)) {
+			BC_ASSERT_STRING_EQUAL(linphone_chat_message_get_text(msg), marie_text);
+			linphone_chat_message_unref(msg);
+		}
+		const LinphoneAddress *local_address = linphone_chat_room_get_local_address(paulineCr);
+		BC_ASSERT_TRUE(linphone_address_has_uri_param(local_address, "gr"));
+		LinphoneChatMessage *sent_msg = _send_message(paulineCr, pauline_text);
+		BC_ASSERT_TRUE(
+		    wait_for_list(coresList, &pauline->stat.number_of_LinphoneMessageSent, 1, liblinphone_tester_sip_timeout));
+		BC_ASSERT_FALSE(wait_for_list(coresList, &marie->stat.number_of_LinphoneChatRoomStateCreated, 2, 2000));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &marie->stat.number_of_LinphoneMessageReceived, 1,
+		                             liblinphone_tester_sip_timeout));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &pauline->stat.number_of_LinphoneMessageDelivered, 1,
+		                             liblinphone_tester_sip_timeout));
+		linphone_chat_message_unref(sent_msg);
+	}
+
+	if (marieCr) {
+		LinphoneChatMessage *msg = linphone_chat_room_get_last_message_in_history(marieCr);
+		if (BC_ASSERT_PTR_NOT_NULL(msg)) {
+			BC_ASSERT_STRING_EQUAL(linphone_chat_message_get_text(msg), pauline_text);
+			linphone_chat_message_unref(msg);
+		}
+		linphone_core_manager_delete_chat_room(marie, marieCr, coresList);
+		linphone_chat_room_unref(marieCr);
+	}
+	if (paulineCr) linphone_core_manager_delete_chat_room(pauline, paulineCr, coresList);
+
+	bctbx_list_free(coresList);
+	bctbx_list_free(coresManagerList);
+
+	linphone_core_manager_destroy(marie);
+	linphone_core_manager_destroy(pauline);
+}
+
+static void basic_chat_room_with_cpim_gruu(void) {
+	basic_chat_room_with_cpim_base(TRUE);
+}
+
+static void basic_chat_room_with_cpim_without_gruu(void) {
+	basic_chat_room_with_cpim_base(FALSE);
+}
+
 static void find_one_to_one_chat_room(void) {
 	LinphoneCoreManager *marie = linphone_core_manager_create("marie_rc");
 	LinphoneCoreManager *pauline = linphone_core_manager_create("pauline_rc");
@@ -9495,6 +9615,8 @@ test_t group_chat2_tests[] = {
     TEST_ONE_TAG("IMDN sent from DB state", imdn_sent_from_db_state, "LeaksMemory"),
     TEST_NO_TAG("IMDN updated for group chat room with one participant offline",
                 imdn_updated_for_group_chat_room_with_one_participant_offline),
+    TEST_NO_TAG("Basic chat room with CPIM and GRUU", basic_chat_room_with_cpim_gruu),
+    TEST_NO_TAG("Basic chat room with CPIM and without GRUU", basic_chat_room_with_cpim_without_gruu),
     TEST_NO_TAG("Find one-to-one chat room", find_one_to_one_chat_room),
     TEST_NO_TAG("Exhumed one-to-one chat room 1", exhume_one_to_one_chat_room_1),
     TEST_NO_TAG("Exhumed one-to-one chat room 2", exhume_one_to_one_chat_room_2),
-- 
GitLab