From 113c67ec95c789c50cc3149a8c0c31ad833da691 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 7 Jan 2023 19:47:51 +0400 Subject: [PATCH] core: disable connections on repeat AUTH errors (#1704) --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 58 +++++++++++++---- src/Simplex/Chat/Controller.hs | 7 +++ .../M20230107_connections_auth_err_counter.hs | 17 +++++ src/Simplex/Chat/Migrations/chat_schema.sql | 1 + src/Simplex/Chat/Store.hs | 63 ++++++++++++------- src/Simplex/Chat/Types.hs | 7 +++ src/Simplex/Chat/View.hs | 48 +++++++++----- tests/ChatTests.hs | 20 ++++++ 9 files changed, 174 insertions(+), 48 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 22948a6215..b8bf55e631 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -73,6 +73,7 @@ library Simplex.Chat.Migrations.M20221222_chat_ts Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status Simplex.Chat.Migrations.M20221230_idxs + Simplex.Chat.Migrations.M20230107_connections_auth_err_counter Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d68177d199..b708e293a9 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -832,6 +832,17 @@ processChatCommand = \case case activeConn of Just conn -> verifyConnectionCode user conn code _ -> throwChatError CEGroupMemberNotActive + APIEnableContact contactId -> withUser $ \user -> do + Contact {activeConn} <- withStore $ \db -> getContact db user contactId + withStore' $ \db -> setConnectionAuthErrCounter db user activeConn 0 + pure CRCmdOk + APIEnableGroupMember gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId + case activeConn of + Just conn -> do + withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 + pure CRCmdOk + _ -> throwChatError CEGroupMemberNotActive ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do chatId <- case cType of CTDirect -> withStore $ \db -> getContactIdByName db user name @@ -846,6 +857,8 @@ processChatCommand = \case GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode VerifyContact cName code -> withContactName cName (`APIVerifyContact` code) VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code + EnableContact cName -> withContactName cName APIEnableContact + EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do @@ -1917,13 +1930,14 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = _ <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId withAckMessage agentConnId cmdId meta $ pure () SENT msgId -> - -- ? updateDirectChatItemStatus sentMsgDeliveryEvent conn msgId OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> toView . CRChatError $ ChatErrorAgent err (Just connEntity) -- ? updateDirectChatItemStatus + MERR _ err -> do + toView . CRChatError $ ChatErrorAgent err (Just connEntity) + incAuthErrCounter connEntity conn err ERR err -> do toView . CRChatError $ ChatErrorAgent err (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -2039,6 +2053,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err) toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) toView . CRChatError $ ChatErrorAgent err (Just connEntity) + incAuthErrCounter connEntity conn err ERR err -> do toView . CRChatError $ ChatErrorAgent err (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -2116,7 +2131,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = updateGroupMemberStatus db userId m GSMemConnected unless (memberActive membership) $ updateGroupMemberStatus db userId membership GSMemConnected - sendPendingGroupMessages m conn + -- possible improvement: check for each pending message, requires keeping track of connection state + unless (connDisabled conn) $ sendPendingGroupMessages m conn withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings case memberCategory m of GCHostMember -> do @@ -2185,7 +2201,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> toView . CRChatError $ ChatErrorAgent err (Just connEntity) + MERR _ err -> do + toView . CRChatError $ ChatErrorAgent err (Just connEntity) + incAuthErrCounter connEntity conn err ERR err -> do toView . CRChatError $ ChatErrorAgent err (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -2276,7 +2294,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \_cmdData -> pure () - MERR _ err -> toView . CRChatError $ ChatErrorAgent err (Just connEntity) + MERR _ err -> do + toView . CRChatError $ ChatErrorAgent err (Just connEntity) + incAuthErrCounter connEntity conn err ERR err -> do toView . CRChatError $ ChatErrorAgent err (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -2334,7 +2354,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = XInfo p -> profileContactRequest invId p Nothing -- TODO show/log error, other events in contact request _ -> pure () - MERR _ err -> toView . CRChatError $ ChatErrorAgent err (Just connEntity) + MERR _ err -> do + toView . CRChatError $ ChatErrorAgent err (Just connEntity) + incAuthErrCounter connEntity conn err ERR err -> do toView . CRChatError $ ChatErrorAgent err (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -2365,6 +2387,15 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = showToast (localDisplayName <> "> ") "wants to connect to you" _ -> pure () + incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> m () + incAuthErrCounter connEntity conn err = do + case err of + SMP SMP.AUTH -> do + authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn + when (authErrCounter' >= authErrDisableCount) $ do + toView $ CRConnectionDisabled connEntity + _ -> pure () + updateChatLock :: MsgEncodingI e => String -> ChatMsgEvent e -> m () updateChatLock name event = do l <- asks chatLock @@ -3282,13 +3313,14 @@ deleteOrUpdateMemberRecord user@User {userId} member = Nothing -> deleteGroupMember db user member sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64) -sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent = do - if connStatus == ConnReady || connStatus == ConnSndReady - then sendDirectMessage conn chatMsgEvent (ConnectionId connId) - else throwChatError $ CEContactNotReady ct +sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent + | connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct + | connDisabled conn = throwChatError $ CEContactDisabled ct + | otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId) sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64) sendDirectMessage conn chatMsgEvent connOrGroupId = do + when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId (msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId @@ -3323,10 +3355,10 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do case memberConn m of Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ Just conn@Connection {connStatus} + | connDisabled conn || connStatus == ConnDeleted -> pure () | connStatus == ConnSndReady || connStatus == ConnReady -> do let tag = toCMEventTag chatMsgEvent (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ()) - | connStatus == ConnDeleted -> pure () | otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ pure msg @@ -3722,10 +3754,14 @@ chatCommandP = "/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal), "/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)), "/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)), + "/_enable @" *> (APIEnableContact <$> A.decimal), + "/_enable #" *> (APIEnableGroupMember <$> A.decimal <* A.space <*> A.decimal), "/code " *> char_ '@' *> (GetContactCode <$> displayName), "/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName), "/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)), "/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)), + "/enable " *> char_ '@' *> (EnableContact <$> displayName), + "/enable #" *> (EnableGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName), ("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles, ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8ce17df96a..8266f49dea 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -210,6 +210,8 @@ data ChatCommand | APIGetGroupMemberCode GroupId GroupMemberId | APIVerifyContact ContactId (Maybe Text) | APIVerifyGroupMember GroupId GroupMemberId (Maybe Text) + | APIEnableContact ContactId + | APIEnableGroupMember GroupId GroupMemberId | ShowMessages ChatName Bool | ContactInfo ContactName | GroupMemberInfo GroupName ContactName @@ -219,6 +221,8 @@ data ChatCommand | GetGroupMemberCode GroupName ContactName | VerifyContact ContactName (Maybe Text) | VerifyGroupMember GroupName ContactName (Maybe Text) + | EnableContact ContactName + | EnableGroupMember GroupName ContactName | ChatHelp HelpSection | Welcome | AddContact @@ -414,6 +418,7 @@ data ChatResponse | CRSQLResult {rows :: [Text]} | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} | CRAgentStats {agentStats :: [[String]]} + | CRConnectionDisabled {connectionEntity :: ConnectionEntity} | CRMessageError {severity :: Text, errorMessage :: Text} | CRChatCmdError {chatError :: ChatError} | CRChatError {chatError :: ChatError} @@ -558,6 +563,8 @@ data ChatErrorType | CEInvalidConnReq | CEInvalidChatMessage {message :: String} | CEContactNotReady {contact :: Contact} + | CEContactDisabled {contact :: Contact} + | CEConnectionDisabled {connection :: Connection} | CEGroupUserRole | CEContactIncognitoCantInvite | CEGroupIncognitoCantInvite diff --git a/src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs b/src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs new file mode 100644 index 0000000000..b3c724e938 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230107_connections_auth_err_counter where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230107_connections_auth_err_counter :: Query +m20230107_connections_auth_err_counter = + [sql| +PRAGMA ignore_check_constraints=ON; + +ALTER TABLE connections ADD COLUMN auth_err_counter INTEGER DEFAULT 0 CHECK (auth_err_counter NOT NULL); +UPDATE connections SET auth_err_counter = 0; + +PRAGMA ignore_check_constraints=OFF; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 8ef2acabfa..84059e9ebf 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -263,6 +263,7 @@ CREATE TABLE connections( group_link_id BLOB, security_code TEXT NULL, security_code_verified_at TEXT NULL, + auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL), FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 796ba00530..1fc311f282 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -48,6 +48,8 @@ module Simplex.Chat.Store updateContactUnreadChat, updateGroupUnreadChat, setConnectionVerified, + incConnectionAuthErrCounter, + setConnectionAuthErrCounter, getUserContacts, getUserContactProfiles, createUserContactLink, @@ -324,6 +326,7 @@ import Simplex.Chat.Migrations.M20221214_live_message import Simplex.Chat.Migrations.M20221222_chat_ts import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status import Simplex.Chat.Migrations.M20221230_idxs +import Simplex.Chat.Migrations.M20230107_connections_auth_err_counter import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) @@ -383,7 +386,8 @@ schemaMigrations = ("20221214_live_message", m20221214_live_message), ("20221222_chat_ts", m20221222_chat_ts), ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status), - ("20221230_idxs", m20221230_idxs) + ("20221230_idxs", m20221230_idxs), + ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter) ] -- | The list of migrations in ascending order by date @@ -495,7 +499,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id @@ -570,7 +574,7 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing} + pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} where ent ct = if connType == ct then entityId else Nothing @@ -764,6 +768,19 @@ setConnectionVerified db User {userId} connId code = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId) +incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int +incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do + updatedAt <- getCurrentTime + (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) + let counter' = fromMaybe authErrCounter counter_ + 1 + DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) + pure counter' + +setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) + updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime @@ -861,7 +878,7 @@ getUserAddressConnections db User {userId} = do db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL @@ -875,7 +892,7 @@ getUserContactLinks db User {userId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -1008,7 +1025,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ? @@ -1112,7 +1129,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id @@ -1311,7 +1328,7 @@ getContactConnections db userId Contact {contactId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? @@ -1322,15 +1339,15 @@ getContactConnections db userId Contact {contactId} = type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int) toConnection :: ConnectionRow -> Connection -toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) = let entityId = entityId_ connType connectionCode = SecurityCode <$> code_ <*> verifiedAt_ - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, createdAt} + in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -1340,8 +1357,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_)) = - Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) toMaybeConnection _ = Nothing getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact] @@ -1512,7 +1529,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter FROM connections WHERE user_id = ? AND agent_conn_id = ? |] @@ -1612,7 +1629,7 @@ getConnectionById db User {userId} connId = ExceptT $ do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ? |] @@ -1657,7 +1674,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -1937,7 +1954,7 @@ groupMemberQuery = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( @@ -2097,7 +2114,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} = cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM contacts ct JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -2402,7 +2419,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -2435,7 +2452,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -3350,7 +3367,7 @@ getDirectChatPreviews_ db user@User {userId} = do cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat, -- ChatItem @@ -3673,7 +3690,7 @@ getContact db user@User {userId} contactId = cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 30ede6ee8d..bf884b7fa6 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1603,10 +1603,17 @@ data Connection = Connection localAlias :: Text, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID connectionCode :: Maybe SecurityCode, + authErrCounter :: Int, createdAt :: UTCTime } deriving (Eq, Show, Generic) +authErrDisableCount :: Int +authErrDisableCount = 10 + +connDisabled :: Connection -> Bool +connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount + data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} deriving (Eq, Show, Generic) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 63278d2339..154d1d8cfc 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -217,6 +217,7 @@ responseToView user_ testView liveItems ts = \case plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) ] CRAgentStats stats -> map (plain . intercalate ",") stats + CRConnectionDisabled entity -> viewConnectionEntityDisabled entity CRMessageError prefix err -> [plain prefix <> ": " <> plain err] CRChatError e -> viewChatError e where @@ -1137,6 +1138,8 @@ viewChatError = \case CEInvalidConnReq -> viewInvalidConnReq CEInvalidChatMessage e -> ["chat message error: " <> sShow e] CEContactNotReady c -> [ttyContact' c <> ": not ready"] + CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)] + CEConnectionDisabled _ -> [] CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"] CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"] CEGroupUserRole -> ["you have insufficient permissions for this group command"] @@ -1201,7 +1204,7 @@ viewChatError = \case DBErrorExport e -> ["error encrypting database: " <> sqliteError' e] DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e] e -> ["chat database error: " <> sShow e] - ChatErrorAgent err entity -> case err of + ChatErrorAgent err entity_ -> case err of SMP SMP.AUTH -> [ withConnEntity <> "error: connection authorization failed - this could happen if connection was deleted,\ @@ -1212,20 +1215,20 @@ viewChatError = \case CONN NOT_FOUND -> [] e -> [withConnEntity <> "smp agent error: " <> sShow e] where - withConnEntity = case entity of - Just (RcvDirectMsgConnection conn contact_) -> case contact_ of - Just Contact {contactId, localDisplayName = c} -> - "[" <> ttyFrom c <> ", contactId: " <> sShow contactId <> ", connId: " <> cId conn <> "] " + withConnEntity = case entity_ of + Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of + Just Contact {contactId} -> + "[" <> connEntityLabel entity <> ", contactId: " <> sShow contactId <> ", connId: " <> cId conn <> "] " Nothing -> - "[" <> ttyFrom "rcv direct msg" <> ", connId: " <> cId conn <> "] " - Just (RcvGroupMsgConnection conn GroupInfo {groupId, localDisplayName = g} GroupMember {groupMemberId, localDisplayName = m}) -> - "[" <> ttyFrom ("#" <> g <> " " <> m) <> ", groupId: " <> sShow groupId <> ", memberId: " <> sShow groupMemberId <> ", connId: " <> cId conn <> "] " - Just (RcvFileConnection conn RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}}) -> - "[" <> ttyFrom ("rcv file " <> T.pack fileName) <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] " - Just (SndFileConnection conn SndFileTransfer {fileId, fileName}) -> - "[" <> ttyTo ("snd file " <> T.pack fileName) <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] " - Just (UserContactConnection conn UserContact {userContactLinkId}) -> - "[" <> ttyFrom "contact address" <> ", userContactLinkId: " <> sShow userContactLinkId <> ", connId: " <> cId conn <> "] " + "[" <> connEntityLabel entity <> ", connId: " <> cId conn <> "] " + Just entity@(RcvGroupMsgConnection conn GroupInfo {groupId} GroupMember {groupMemberId}) -> + "[" <> connEntityLabel entity <> ", groupId: " <> sShow groupId <> ", memberId: " <> sShow groupMemberId <> ", connId: " <> cId conn <> "] " + Just entity@(RcvFileConnection conn RcvFileTransfer {fileId}) -> + "[" <> connEntityLabel entity <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] " + Just entity@(SndFileConnection conn SndFileTransfer {fileId}) -> + "[" <> connEntityLabel entity <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] " + Just entity@(UserContactConnection conn UserContact {userContactLinkId}) -> + "[" <> connEntityLabel entity <> ", userContactLinkId: " <> sShow userContactLinkId <> ", connId: " <> cId conn <> "] " Nothing -> "" cId conn = sShow (connId (conn :: Connection)) where @@ -1234,6 +1237,23 @@ viewChatError = \case SQLiteErrorNotADatabase -> "wrong passphrase or invalid database file" SQLiteError e -> sShow e +viewConnectionEntityDisabled :: ConnectionEntity -> [StyledString] +viewConnectionEntityDisabled entity = case entity of + RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)] + RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> g <> " " <> m)] + _ -> ["[" <> entityLabel <> "] connection is disabled"] + where + entityLabel = connEntityLabel entity + +connEntityLabel :: ConnectionEntity -> StyledString +connEntityLabel = \case + RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c + RcvDirectMsgConnection _ Nothing -> "rcv direct msg" + RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> plain $ "#" <> g <> " " <> m + RcvFileConnection _ RcvFileTransfer {fileInvitation = FileInvitation {fileName}} -> plain $ "rcv file " <> T.pack fileName + SndFileConnection _ SndFileTransfer {fileName} -> plain $ "snd file " <> T.pack fileName + UserContactConnection _ UserContact {} -> "contact address" + ttyContact :: ContactName -> StyledString ttyContact = styled $ colored Green diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index c4aac65bf6..1987d97507 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -58,6 +58,7 @@ chatTests = do it "direct message update" testDirectMessageUpdate it "direct message delete" testDirectMessageDelete it "direct live message" testDirectLiveMessage + it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact describe "chat groups" $ do describe "add contacts, create group and send/receive messages" testGroup it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages @@ -514,6 +515,25 @@ testDirectLiveMessage = alice <# "@bob [LIVE] hello 2" bob <# "alice> [LIVE ended] hello 2" +testRepeatAuthErrorsDisableContact :: IO () +testRepeatAuthErrorsDisableContact = + testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice <##> bob + bob ##> "/d alice" + bob <## "alice: contact is deleted" + forM_ [1 .. authErrDisableCount] $ \_ -> sendAuth alice + alice <## "[bob] connection is disabled, to enable: /enable bob, to delete: /d bob" + alice ##> "@bob hey" + alice <## "bob: disabled, to enable: /enable bob, to delete: /d bob" + alice ##> "/enable bob" + alice <## "ok" + sendAuth alice + where + sendAuth alice = do + alice #> "@bob hey" + alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" + testGroup :: Spec testGroup = versionTestMatrix3 runTestGroup where