core: do not regenerate key when accepting connection to avoid invalidating invitation link on bad networks (#5018)

* core: prepare conn (plan)

* update

* group join

* comment

* comment

* wip

* Revert "wip"

This reverts commit 0849f43377.

* accept

* save contact_id, reuse contact

* refactor

* simplexmq

* set contactUsed

* support retrying join

* exclude prepared connections from API responses

* avoid race with events

* avoid race better

* fix UI

* update library

* tmp

* update

* display error details on ios cmd prohibited

* underscore instead of empty

* Update apps/ios/Shared/Model/SimpleXAPI.swift

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* test

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
This commit is contained in:
spaced4ndy
2024-10-11 15:37:38 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin Diogo
parent 2127c7dcce
commit 9a87f344b5
21 changed files with 401 additions and 91 deletions
+4
View File
@@ -1416,6 +1416,10 @@ catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError
catchStoreError = catchAllErrors mkStoreError
{-# INLINE catchStoreError #-}
tryStoreError' :: ExceptT StoreError IO a -> IO (Either StoreError a)
tryStoreError' = tryAllErrors' mkStoreError
{-# INLINE tryStoreError' #-}
mkStoreError :: SomeException -> StoreError
mkStoreError = SEInternalError . show
{-# INLINE mkStoreError #-}
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20241010_contact_requests_contact_id where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20241010_contact_requests_contact_id :: Query
m20241010_contact_requests_contact_id =
[sql|
ALTER TABLE contact_requests ADD COLUMN contact_id INTEGER REFERENCES contacts ON DELETE CASCADE;
CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id);
|]
down_m20241010_contact_requests_contact_id :: Query
down_m20241010_contact_requests_contact_id =
[sql|
DROP INDEX idx_contact_requests_contact_id;
ALTER TABLE contact_requests DROP COLUMN contact_id;
|]
@@ -327,6 +327,7 @@ CREATE TABLE contact_requests(
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
pq_support INTEGER NOT NULL DEFAULT 0,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON UPDATE CASCADE
@@ -888,3 +889,4 @@ CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(
CREATE INDEX idx_received_probes_group_member_id on received_probes(
group_member_id
);
CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id);
+11
View File
@@ -125,6 +125,17 @@ data ConnectionEntity
$(JQ.deriveJSON (sumTypeJSON fstToLower) ''ConnectionEntity)
connEntityInfo :: ConnectionEntity -> String
connEntityInfo = \case
RcvDirectMsgConnection c ct_ -> ctInfo ct_ <> ", status: " <> show (connStatus c)
RcvGroupMsgConnection c g m -> mInfo g m <> ", status: " <> show (connStatus c)
SndFileConnection c _ft -> "snd file, status: " <> show (connStatus c)
RcvFileConnection c _ft -> "rcv file, status: " <> show (connStatus c)
UserContactConnection c _uc -> "user address, status: " <> show (connStatus c)
where
ctInfo = maybe "connection" $ \Contact {contactId} -> "contact " <> show contactId
mInfo GroupInfo {groupId} GroupMember {groupMemberId} = "group " <> show groupId <> ", member " <> show groupMemberId
updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity
updateEntityConnStatus connEntity connStatus = case connEntity of
RcvDirectMsgConnection c ct_ -> RcvDirectMsgConnection (st c) ((\ct -> (ct :: Contact) {activeConn = Just $ st c}) <$> ct_)
+53 -28
View File
@@ -62,6 +62,8 @@ module Simplex.Chat.Store.Direct
getContactRequestIdByName,
deleteContactRequest,
createAcceptedContact,
deleteContactRequestRec,
updateContactAccepted,
getUserByContactRequestId,
getPendingContactConnections,
updatePCCUser,
@@ -69,6 +71,7 @@ module Simplex.Chat.Store.Direct
getConnectionById,
getConnectionsContacts,
updateConnectionStatus,
updateConnectionStatusFromTo,
updateContactSettings,
setConnConnReqInv,
resetContactConnInitiated,
@@ -655,7 +658,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
db
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
@@ -724,7 +727,7 @@ getContactRequest db User {userId} contactRequestId =
db
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
@@ -766,9 +769,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId, userId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createAcceptedContact :: DB.Connection -> User -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connStatus connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection)
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case
NewIncognito p -> createIncognitoProfile_ db userId createdAt p
@@ -779,29 +781,42 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed)
contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connStatus connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName)
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $
Contact
{ contactId,
localDisplayName,
profile = toLocalProfile profileId profile "",
activeConn = Just conn,
viaGroup = Nothing,
contactUsed,
contactStatus = CSActive,
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt,
updatedAt = createdAt,
chatTs = Just createdAt,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
uiThemes = Nothing,
chatDeleted = False,
customData = Nothing
}
ct =
Contact
{ contactId,
localDisplayName,
profile = toLocalProfile profileId profile "",
activeConn = Just conn,
viaGroup = Nothing,
contactUsed,
contactStatus = CSActive,
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt,
updatedAt = createdAt,
chatTs = Just createdAt,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
uiThemes = Nothing,
chatDeleted = False,
customData = Nothing
}
pure (ct, conn)
deleteContactRequestRec :: DB.Connection -> User -> UserContactRequest -> IO ()
deleteContactRequestRec db User {userId} UserContactRequest {contactRequestId} =
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO ()
updateContactAccepted db User {userId} Contact {contactId} contactUsed =
DB.execute
db
"UPDATE contacts SET contact_used = ? WHERE user_id = ? AND contact_id = ?"
(contactUsed, userId, contactId)
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName =
@@ -927,7 +942,17 @@ getConnectionsContacts db agentConnIds = do
toContactRef (contactId, connId, acId, localDisplayName) = ContactRef {contactId, connId, agentConnId = AgentConnId acId, localDisplayName}
updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO ()
updateConnectionStatus db Connection {connId} connStatus = do
updateConnectionStatus db Connection {connId} = updateConnectionStatus_ db connId
{-# INLINE updateConnectionStatus #-}
updateConnectionStatusFromTo :: DB.Connection -> Int64 -> ConnStatus -> ConnStatus -> IO ()
updateConnectionStatusFromTo db connId fromStatus toStatus = do
maybeFirstRow fromOnly (DB.query db "SELECT conn_status FROM connections WHERE connection_id = ?" (Only connId)) >>= \case
Just status | status == fromStatus -> updateConnectionStatus_ db connId toStatus
_ -> pure ()
updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO ()
updateConnectionStatus_ db connId connStatus = do
currentTs <- getCurrentTime
if connStatus == ConnReady
then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId)
+3 -2
View File
@@ -882,7 +882,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of
db
( [sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences,
cr.created_at, cr.updated_at as ts,
cr.peer_chat_min_version, cr.peer_chat_max_version
@@ -930,6 +930,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_contact
AND conn_status != :conn_status
AND contact_id IS NULL
AND conn_level = 0
AND via_contact IS NULL
@@ -938,7 +939,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
|]
<> pagQuery
)
([":user_id" := userId, ":conn_contact" := ConnContact, ":search" := search] <> pagParams)
([":user_id" := userId, ":conn_contact" := ConnContact, ":conn_status" := ConnPrepared, ":search" := search] <> pagParams)
toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData
toPreview connRow =
let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow
+3 -1
View File
@@ -113,6 +113,7 @@ import Simplex.Chat.Migrations.M20240528_quota_err_counter
import Simplex.Chat.Migrations.M20240827_calls_uuid
import Simplex.Chat.Migrations.M20240920_user_order
import Simplex.Chat.Migrations.M20241008_indexes
import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -225,7 +226,8 @@ schemaMigrations =
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter),
("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid),
("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order),
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes)
("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes),
("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id)
]
-- | The list of migrations in ascending order by date
+3 -3
View File
@@ -411,13 +411,13 @@ getProfileById db userId profileId =
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
toContactRequest :: ContactRequestRow -> UserContactRequest
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do
let profile = Profile {displayName, fullName, image, contactLink, preferences}
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt}
in UserContactRequest {contactRequestId, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt}
userQuery :: Query
userQuery =
+5
View File
@@ -297,6 +297,7 @@ userContactGroupId UserContact {groupId} = groupId
data UserContactRequest = UserContactRequest
{ contactRequestId :: Int64,
agentInvitationId :: AgentInvId,
contactId_ :: Maybe ContactId,
userContactLinkId :: Int64,
agentContactConnId :: AgentConnId, -- connection id of user contact
cReqChatVRange :: VersionRangeChat,
@@ -1371,6 +1372,8 @@ aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId
data ConnStatus
= -- | connection is created by initiating party with agent NEW command (createConnection)
ConnNew
| -- | connection is prepared, to avoid changing keys on invitation links when retrying.
ConnPrepared
| -- | connection is joined by joining party with agent JOIN command (joinConnection)
ConnJoined
| -- | initiating party received CONF notification (to be renamed to REQ)
@@ -1399,6 +1402,7 @@ instance ToJSON ConnStatus where
instance TextEncoding ConnStatus where
textDecode = \case
"new" -> Just ConnNew
"prepared" -> Just ConnPrepared
"joined" -> Just ConnJoined
"requested" -> Just ConnRequested
"accepted" -> Just ConnAccepted
@@ -1408,6 +1412,7 @@ instance TextEncoding ConnStatus where
_ -> Nothing
textEncode = \case
ConnNew -> "new"
ConnPrepared -> "prepared"
ConnJoined -> "joined"
ConnRequested -> "requested"
ConnAccepted -> "accepted"