diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index 14738c154..92bdfded4 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -3,7 +3,6 @@ module Main where import Control.Logger.Simple -import Data.Maybe import Simplex.Messaging.Server.Main import System.Environment diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 6dac14717..880822ba0 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -148,7 +148,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..)) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) -import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SProtocolType (..), SndPublicVerifyKey, UserProtocol, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SubscriptionMode (..), SMPMsgMeta, SProtocolType (..), SndPublicVerifyKey, UserProtocol, XFTPServerWithAuth) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util @@ -189,20 +189,20 @@ deleteUser :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> m () deleteUser c = withAgentEnv c .: deleteUser' c -- | Create SMP agent connection (NEW command) asynchronously, synchronous response is new connection id -createConnectionAsync :: forall m c. (AgentErrorMonad m, ConnectionModeI c) => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> m ConnId -createConnectionAsync c userId corrId enableNtfs cMode = withAgentEnv c $ newConnAsync c userId corrId enableNtfs cMode +createConnectionAsync :: forall m c. (AgentErrorMonad m, ConnectionModeI c) => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> SubscriptionMode -> m ConnId +createConnectionAsync c userId aCorrId enableNtfs = withAgentEnv c .: newConnAsync c userId aCorrId enableNtfs -- | Join SMP agent connection (JOIN command) asynchronously, synchronous response is new connection id -joinConnectionAsync :: AgentErrorMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> m ConnId -joinConnectionAsync c userId corrId enableNtfs = withAgentEnv c .: joinConnAsync c userId corrId enableNtfs +joinConnectionAsync :: AgentErrorMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId +joinConnectionAsync c userId aCorrId enableNtfs = withAgentEnv c .:. joinConnAsync c userId aCorrId enableNtfs -- | Allow connection to continue after CONF notification (LET command), no synchronous response allowConnectionAsync :: AgentErrorMonad m => AgentClient -> ACorrId -> ConnId -> ConfirmationId -> ConnInfo -> m () allowConnectionAsync c = withAgentEnv c .:: allowConnectionAsync' c -- | Accept contact after REQ notification (ACPT command) asynchronously, synchronous response is new connection id -acceptContactAsync :: AgentErrorMonad m => AgentClient -> ACorrId -> Bool -> ConfirmationId -> ConnInfo -> m ConnId -acceptContactAsync c corrId enableNtfs = withAgentEnv c .: acceptContactAsync' c corrId enableNtfs +acceptContactAsync :: AgentErrorMonad m => AgentClient -> ACorrId -> Bool -> ConfirmationId -> ConnInfo -> SubscriptionMode -> m ConnId +acceptContactAsync c aCorrId enableNtfs = withAgentEnv c .:. acceptContactAsync' c aCorrId enableNtfs -- | Acknowledge message (ACK command) asynchronously, no synchronous response ackMessageAsync :: forall m. AgentErrorMonad m => AgentClient -> ACorrId -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> m () @@ -221,20 +221,20 @@ deleteConnectionsAsync :: AgentErrorMonad m => AgentClient -> [ConnId] -> m () deleteConnectionsAsync c = withAgentEnv c . deleteConnectionsAsync' c -- | Create SMP agent connection (NEW command) -createConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe CRClientData -> m (ConnId, ConnectionRequestUri c) -createConnection c userId enableNtfs cMode clientData = withAgentEnv c $ newConn c userId "" enableNtfs cMode clientData +createConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> m (ConnId, ConnectionRequestUri c) +createConnection c userId enableNtfs = withAgentEnv c .:. newConn c userId "" enableNtfs -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> m ConnId -joinConnection c userId enableNtfs = withAgentEnv c .: joinConn c userId "" enableNtfs +joinConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId +joinConnection c userId enableNtfs = withAgentEnv c .:. joinConn c userId "" enableNtfs -- | Allow connection to continue after CONF notification (LET command) allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () allowConnection c = withAgentEnv c .:. allowConnection' c -- | Accept contact after REQ notification (ACPT command) -acceptContact :: AgentErrorMonad m => AgentClient -> Bool -> ConfirmationId -> ConnInfo -> m ConnId -acceptContact c enableNtfs = withAgentEnv c .: acceptContact' c "" enableNtfs +acceptContact :: AgentErrorMonad m => AgentClient -> Bool -> ConfirmationId -> ConnInfo -> SubscriptionMode -> m ConnId +acceptContact c enableNtfs = withAgentEnv c .:. acceptContact' c "" enableNtfs -- | Reject contact (RJCT command) rejectContact :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> m () @@ -427,10 +427,10 @@ client c@AgentClient {rcvQ, subQ} = forever $ do processCommand :: forall m. AgentMonad m => AgentClient -> (EntityId, APartyCmd 'Client) -> m (EntityId, APartyCmd 'Agent) processCommand c (connId, APC e cmd) = second (APC e) <$> case cmd of - NEW enableNtfs (ACM cMode) -> second (INV . ACR cMode) <$> newConn c userId connId enableNtfs cMode Nothing - JOIN enableNtfs (ACR _ cReq) connInfo -> (,OK) <$> joinConn c userId connId enableNtfs cReq connInfo + NEW enableNtfs (ACM cMode) subMode -> second (INV . ACR cMode) <$> newConn c userId connId enableNtfs cMode Nothing subMode + JOIN enableNtfs (ACR _ cReq) subMode connInfo -> (,OK) <$> joinConn c userId connId enableNtfs cReq connInfo subMode LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) - ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId True invId ownCInfo + ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId True invId ownCInfo SMSubscribe RJCT invId -> rejectContact' c connId invId $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgFlags msgBody -> (connId,) . MID <$> sendMessage' c connId msgFlags msgBody @@ -462,10 +462,10 @@ deleteUser' c userId delSMPQueues = do whenM (withStore' c (`deleteUserWithoutConns` userId)) $ atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ DEL_USER userId) -newConnAsync :: forall m c. (AgentMonad m, ConnectionModeI c) => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> m ConnId -newConnAsync c userId corrId enableNtfs cMode = do +newConnAsync :: forall m c. (AgentMonad m, ConnectionModeI c) => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> SubscriptionMode -> m ConnId +newConnAsync c userId corrId enableNtfs cMode subMode = do connId <- newConnNoQueues c userId "" enableNtfs cMode - enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ NEW enableNtfs (ACM cMode) + enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ NEW enableNtfs (ACM cMode) subMode pure connId newConnNoQueues :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> m ConnId @@ -476,8 +476,8 @@ newConnNoQueues c userId connId enableNtfs cMode = do let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, duplexHandshake = Nothing, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk} withStore c $ \db -> createNewConn db g cData cMode -joinConnAsync :: AgentMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> m ConnId -joinConnAsync c userId corrId enableNtfs cReqUri@(CRInvitationUri ConnReqUriData {crAgentVRange} _) cInfo = do +joinConnAsync :: AgentMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId +joinConnAsync c userId corrId enableNtfs cReqUri@(CRInvitationUri ConnReqUriData {crAgentVRange} _) cInfo subMode = do aVRange <- asks $ smpAgentVRange . config case crAgentVRange `compatibleVersion` aVRange of Just (Compatible connAgentVersion) -> do @@ -485,10 +485,10 @@ joinConnAsync c userId corrId enableNtfs cReqUri@(CRInvitationUri ConnReqUriData let duplexHS = connAgentVersion /= 1 cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, duplexHandshake = Just duplexHS, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk} connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation - enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) cInfo + enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) subMode cInfo pure connId _ -> throwError $ AGENT A_VERSION -joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _cInfo = +joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo = throwError $ CMD PROHIBITED allowConnectionAsync' :: AgentMonad m => AgentClient -> ACorrId -> ConnId -> ConfirmationId -> ConnInfo -> m () @@ -498,13 +498,13 @@ allowConnectionAsync' c corrId connId confId ownConnInfo = enqueueCommand c corrId connId (Just server) $ AClientCommand $ APC SAEConn $ LET confId ownConnInfo _ -> throwError $ CMD PROHIBITED -acceptContactAsync' :: AgentMonad m => AgentClient -> ACorrId -> Bool -> InvitationId -> ConnInfo -> m ConnId -acceptContactAsync' c corrId enableNtfs invId ownConnInfo = do +acceptContactAsync' :: AgentMonad m => AgentClient -> ACorrId -> Bool -> InvitationId -> ConnInfo -> SubscriptionMode -> m ConnId +acceptContactAsync' c corrId enableNtfs invId ownConnInfo subMode = do Invitation {contactConnId, connReq} <- withStore c (`getInvitation` invId) withStore c (`getConn` contactConnId) >>= \case SomeConn _ (ContactConnection ConnData {userId} _) -> do withStore' c $ \db -> acceptInvitation db invId ownConnInfo - joinConnAsync c userId corrId enableNtfs connReq ownConnInfo `catchAgentError` \err -> do + joinConnAsync c userId corrId enableNtfs connReq ownConnInfo subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err _ -> throwError $ CMD PROHIBITED @@ -558,21 +558,23 @@ switchConnectionAsync' c corrId connId = pure . connectionStats $ DuplexConnection cData rqs' sqs _ -> throwError $ CMD PROHIBITED -newConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> m (ConnId, ConnectionRequestUri c) -newConn c userId connId enableNtfs cMode clientData = - getSMPServer c userId >>= newConnSrv c userId connId enableNtfs cMode clientData +newConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> m (ConnId, ConnectionRequestUri c) +newConn c userId connId enableNtfs cMode clientData subMode = + getSMPServer c userId >>= newConnSrv c userId connId enableNtfs cMode clientData subMode -newConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SMPServerWithAuth -> m (ConnId, ConnectionRequestUri c) -newConnSrv c userId connId enableNtfs cMode clientData srv = do +newConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> SMPServerWithAuth -> m (ConnId, ConnectionRequestUri c) +newConnSrv c userId connId enableNtfs cMode clientData subMode srv = do connId' <- newConnNoQueues c userId connId enableNtfs cMode - newRcvConnSrv c userId connId' enableNtfs cMode clientData srv + newRcvConnSrv c userId connId' enableNtfs cMode clientData subMode srv -newRcvConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SMPServerWithAuth -> m (ConnId, ConnectionRequestUri c) -newRcvConnSrv c userId connId enableNtfs cMode clientData srv = do +newRcvConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> SMPServerWithAuth -> m (ConnId, ConnectionRequestUri c) +newRcvConnSrv c userId connId enableNtfs cMode clientData subMode srv = do AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri) <- newRcvQueue c userId connId srv smpClientVRange `catchAgentError` \e -> liftIO (print e) >> throwError e + (rq, qUri) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e void . withStore c $ \db -> updateNewConnRcv db connId rq - addSubscription c rq + case subMode of + SMOnlyCreate -> pure () + SMSubscribe -> addSubscription c rq when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) @@ -584,13 +586,13 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData srv = do withStore' c $ \db -> createRatchetX3dhKeys db connId pk1 pk2 pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eEncryptVRange) -joinConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> m ConnId -joinConn c userId connId enableNtfs cReq cInfo = do +joinConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId +joinConn c userId connId enableNtfs cReq cInfo subMode = do srv <- case cReq of CRInvitationUri ConnReqUriData {crSmpQueues = q :| _} _ -> getNextServer c userId [qServer q] _ -> getSMPServer c userId - joinConnSrv c userId connId enableNtfs cReq cInfo srv + joinConnSrv c userId connId enableNtfs cReq cInfo subMode srv startJoinInvitation :: AgentMonad m => UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> m (Compatible Version, ConnData, SndQueue, CR.Ratchet 'C.X448, CR.E2ERatchetParams 'C.X448) startJoinInvitation userId connId enableNtfs (CRInvitationUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)} e2eRcvParamsUri) = do @@ -609,8 +611,8 @@ startJoinInvitation userId connId enableNtfs (CRInvitationUri ConnReqUriData {cr pure (aVersion, cData, q, rc, e2eSndParams) _ -> throwError $ AGENT A_VERSION -joinConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SMPServerWithAuth -> m ConnId -joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo srv = do +joinConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> SMPServerWithAuth -> m ConnId +joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo subMode srv = do (aVersion, cData@ConnData {connAgentVersion}, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv g <- asks idsDrg connId' <- withStore c $ \db -> runExceptT $ do @@ -620,7 +622,7 @@ joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo srv = do let sq = (q :: SndQueue) {connId = connId'} cData' = (cData :: ConnData) {connId = connId'} duplexHS = connAgentVersion /= 1 - tryError (confirmQueue aVersion c cData' sq srv cInfo $ Just e2eSndParams) >>= \case + tryError (confirmQueue aVersion c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case Right _ -> do unless duplexHS . void $ enqueueMessage c cData' sq SMP.noMsgFlags HELLO pure connId' @@ -628,34 +630,36 @@ joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo srv = do -- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md withStore' c (`deleteConn` connId') throwError e -joinConnSrv c userId connId enableNtfs (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) cInfo srv = do +joinConnSrv c userId connId enableNtfs (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) cInfo subMode srv = do aVRange <- asks $ smpAgentVRange . config clientVRange <- asks $ smpClientVRange . config case ( qUri `compatibleVersion` clientVRange, crAgentVRange `compatibleVersion` aVRange ) of (Just qInfo, Just vrsn) -> do - (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing srv + (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing subMode srv sendInvitation c userId qInfo vrsn cReq cInfo pure connId' _ -> throwError $ AGENT A_VERSION -joinConnSrvAsync :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SMPServerWithAuth -> m () -joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo srv = do +joinConnSrvAsync :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> SMPServerWithAuth -> m () +joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo subMode srv = do (aVersion, cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv dbQueueId <- withStore c $ \db -> runExceptT $ do liftIO $ createRatchet db connId rc ExceptT $ updateNewConnSnd db connId q let q' = (q :: SndQueue) {dbQueueId} - confirmQueueAsync aVersion c cData q' srv cInfo $ Just e2eSndParams -joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _srv = do + confirmQueueAsync aVersion c cData q' srv cInfo (Just e2eSndParams) subMode +joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode _srv = do throwError $ CMD PROHIBITED -createReplyQueue :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> m SMPQueueInfo -createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} srv = do - (rq, qUri) <- newRcvQueue c userId connId srv $ versionToRange smpClientVersion +createReplyQueue :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> m SMPQueueInfo +createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do + (rq, qUri) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode let qInfo = toVersionT qUri smpClientVersion - addSubscription c rq + case subMode of + SMOnlyCreate -> pure () + SMSubscribe -> addSubscription c rq void . withStore c $ \db -> upgradeSndConnToDuplex db connId rq when enableNtfs $ do ns <- asks ntfSupervisor @@ -677,13 +681,13 @@ allowConnection' c connId confId ownConnInfo = withConnLock c connId "allowConne _ -> throwError $ CMD PROHIBITED -- | Accept contact (ACPT command) in Reader monad -acceptContact' :: AgentMonad m => AgentClient -> ConnId -> Bool -> InvitationId -> ConnInfo -> m ConnId -acceptContact' c connId enableNtfs invId ownConnInfo = withConnLock c connId "acceptContact" $ do +acceptContact' :: AgentMonad m => AgentClient -> ConnId -> Bool -> InvitationId -> ConnInfo -> SubscriptionMode -> m ConnId +acceptContact' c connId enableNtfs invId ownConnInfo subMode = withConnLock c connId "acceptContact" $ do Invitation {contactConnId, connReq} <- withStore c (`getInvitation` invId) withStore c (`getConn` contactConnId) >>= \case SomeConn _ (ContactConnection ConnData {userId} _) -> do withStore' c $ \db -> acceptInvitation db invId ownConnInfo - joinConn c userId connId enableNtfs connReq ownConnInfo `catchAgentError` \err -> do + joinConn c userId connId enableNtfs connReq ownConnInfo subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err _ -> throwError $ CMD PROHIBITED @@ -891,16 +895,16 @@ runCommandProcessing c@AgentClient {subQ} server_ = do processCmd :: RetryInterval -> AsyncCmdId -> PendingCommand -> m () processCmd ri cmdId PendingCommand {corrId, userId, connId, command} = case command of AClientCommand (APC _ cmd) -> case cmd of - NEW enableNtfs (ACM cMode) -> noServer $ do + NEW enableNtfs (ACM cMode) subMode -> noServer $ do usedSrvs <- newTVarIO ([] :: [SMPServer]) tryCommand . withNextSrv c userId usedSrvs [] $ \srv -> do - (_, cReq) <- newRcvConnSrv c userId connId enableNtfs cMode Nothing srv + (_, cReq) <- newRcvConnSrv c userId connId enableNtfs cMode Nothing subMode srv notify $ INV (ACR cMode cReq) - JOIN enableNtfs (ACR _ cReq@(CRInvitationUri ConnReqUriData {crSmpQueues = q :| _} _)) connInfo -> noServer $ do + JOIN enableNtfs (ACR _ cReq@(CRInvitationUri ConnReqUriData {crSmpQueues = q :| _} _)) subMode connInfo -> noServer $ do let initUsed = [qServer q] usedSrvs <- newTVarIO initUsed tryCommand . withNextSrv c userId usedSrvs initUsed $ \srv -> do - joinConnSrvAsync c userId connId enableNtfs cReq connInfo srv + joinConnSrvAsync c userId connId enableNtfs cReq connInfo subMode srv notify OK LET confId ownCInfo -> withServer' . tryCommand $ allowConnection' c connId confId ownCInfo >> notify OK ACK msgId rcptInfo_ -> withServer' . tryCommand $ ackMessage' c connId msgId rcptInfo_ >> notify OK @@ -1173,7 +1177,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} cData@ConnData {userId, connId, dupl -- so the condition is not necessary here, strictly speaking. _ -> unless (duplexHandshake == Just True) $ do srv <- getSMPServer c userId - qInfo <- createReplyQueue c cData sq srv + qInfo <- createReplyQueue c cData sq SMSubscribe srv void . enqueueMessage c cData sq SMP.noMsgFlags $ REPLY [qInfo] AM_A_MSG_ -> notify $ SENT mId AM_A_RCVD_ -> pure () @@ -1291,7 +1295,7 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth - (q, qUri) <- newRcvQueue c userId connId srv' clientVRange + (q, qUri) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe let rq' = (q :: RcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} void . withStore c $ \db -> addConnRcvQueue db connId rq' addSubscription c rq' @@ -2318,15 +2322,15 @@ connectReplyQueues c cData@ConnData {userId, connId} ownConnInfo (qInfo :| _) = dbQueueId <- withStore c $ \db -> upgradeRcvConnToDuplex db connId sq enqueueConfirmation c cData sq {dbQueueId} ownConnInfo Nothing -confirmQueueAsync :: forall m. AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m () -confirmQueueAsync v c cData sq srv connInfo e2eEncryption_ = do +confirmQueueAsync :: forall m. AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> SubscriptionMode -> m () +confirmQueueAsync v c cData sq srv connInfo e2eEncryption_ subMode = do resumeMsgDelivery c cData sq - msgId <- storeConfirmation c cData sq e2eEncryption_ =<< mkAgentConfirmation v c cData sq srv connInfo + msgId <- storeConfirmation c cData sq e2eEncryption_ =<< mkAgentConfirmation v c cData sq srv connInfo subMode queuePendingMsgs c sq [msgId] -confirmQueue :: forall m. AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m () -confirmQueue v@(Compatible agentVersion) c cData@ConnData {connId} sq srv connInfo e2eEncryption_ = do - msg <- mkConfirmation =<< mkAgentConfirmation v c cData sq srv connInfo +confirmQueue :: forall m. AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> SubscriptionMode -> m () +confirmQueue v@(Compatible agentVersion) c cData@ConnData {connId} sq srv connInfo e2eEncryption_ subMode = do + msg <- mkConfirmation =<< mkAgentConfirmation v c cData sq srv connInfo subMode sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed where @@ -2336,11 +2340,11 @@ confirmQueue v@(Compatible agentVersion) c cData@ConnData {connId} sq srv connIn encConnInfo <- agentRatchetEncrypt db connId (smpEncode aMessage) e2eEncConnInfoLength pure . smpEncode $ AgentConfirmation {agentVersion, e2eEncryption_, encConnInfo} -mkAgentConfirmation :: AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> m AgentMessage -mkAgentConfirmation (Compatible agentVersion) c cData sq srv connInfo +mkAgentConfirmation :: AgentMonad m => Compatible Version -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> SubscriptionMode -> m AgentMessage +mkAgentConfirmation (Compatible agentVersion) c cData sq srv connInfo subMode | agentVersion == 1 = pure $ AgentConnInfo connInfo | otherwise = do - qInfo <- createReplyQueue c cData sq srv + qInfo <- createReplyQueue c cData sq subMode srv pure $ AgentConnInfoReply (qInfo :| []) connInfo enqueueConfirmation :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m () diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 6ad86aa26..89849d246 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -182,6 +182,7 @@ import Simplex.Messaging.Protocol SMPMsgMeta (..), SProtocolType (..), SndPublicVerifyKey, + SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, @@ -749,7 +750,7 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do (sKey, _) <- C.generateSignatureKeyPair a (dhKey, _) <- C.generateKeyPair' r <- runExceptT $ do - SMP.QIK {rcvId} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp rpKey rKey dhKey auth + SMP.QIK {rcvId} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp rpKey rKey dhKey auth SMSubscribe liftError (testErr TSSecureQueue) $ secureSMPQueue smp rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp @@ -823,8 +824,8 @@ mkSMPTSession q = mkTSession (qUserId q) (qServer q) (qConnId q) getSessionMode :: AgentMonad' m => AgentClient -> m TransportSessionMode getSessionMode = fmap sessionMode . readTVarIO . useNetworkConfig -newRcvQueue :: AgentMonad m => AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRange -> m (RcvQueue, SMPQueueUri) -newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange = do +newRcvQueue :: AgentMonad m => AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRange -> SubscriptionMode -> m (RcvQueue, SMPQueueUri) +newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do C.SignAlg a <- asks (cmdSignAlg . config) (recipientKey, rcvPrivateKey) <- liftIO $ C.generateSignatureKeyPair a (dhKey, privDhKey) <- liftIO C.generateKeyPair' @@ -832,7 +833,7 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange = do logServer "-->" c srv "" "NEW" tSess <- mkTransportSession c userId srv connId QIK {rcvId, sndId, rcvPublicDhKey} <- - withClient c tSess "NEW" $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey dhKey auth + withClient c tSess "NEW" $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey dhKey auth subMode logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index e4c720aef..3053132b9 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -197,6 +197,7 @@ import Simplex.Messaging.Protocol SMPServerWithAuth, SndPublicVerifyKey, SrvLoc (..), + SubscriptionMode, legacyEncodeServer, legacyServerP, legacyStrEncodeServer, @@ -313,9 +314,9 @@ type ConnInfo = ByteString -- | Parameterized type for SMP agent protocol commands and responses from all participants. data ACommand (p :: AParty) (e :: AEntity) where - NEW :: Bool -> AConnectionMode -> ACommand Client AEConn -- response INV + NEW :: Bool -> AConnectionMode -> SubscriptionMode -> ACommand Client AEConn -- response INV INV :: AConnectionRequestUri -> ACommand Agent AEConn - JOIN :: Bool -> AConnectionRequestUri -> ConnInfo -> ACommand Client AEConn -- response OK + JOIN :: Bool -> AConnectionRequestUri -> SubscriptionMode -> ConnInfo -> ACommand Client AEConn -- response OK CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake LET :: ConfirmationId -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client REQ :: InvitationId -> NonEmpty SMPServer -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender @@ -337,7 +338,7 @@ data ACommand (p :: AParty) (e :: AEntity) where MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn ACK :: AgentMsgId -> Maybe MsgReceiptInfo -> ACommand Client AEConn - RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn + RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn SWCH :: ACommand Client AEConn OFF :: ACommand Client AEConn DEL :: ACommand Client AEConn @@ -998,7 +999,7 @@ data AMessage REPLY (NonEmpty SMPQueueInfo) | -- | agent envelope for the client message A_MSG MsgBody - | -- | agent envelope for delivery receipt + | -- | agent envelope for delivery receipt A_RCVD (NonEmpty AMessageReceipt) | -- | the message instructing the client to continue sending messages (after ERR QUOTA) QCONT SndQAddr @@ -1736,8 +1737,8 @@ commandP binaryP = >>= \case ACmdTag SClient e cmd -> ACmd SClient e <$> case cmd of - NEW_ -> s (NEW <$> strP_ <*> strP) - JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> binaryP) + NEW_ -> s (NEW <$> strP_ <*> strP_ <*> strP) + JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> strP_ <*> binaryP) LET_ -> s (LET <$> A.takeTill (== ' ') <* A.space <*> binaryP) ACPT_ -> s (ACPT <$> A.takeTill (== ' ') <* A.space <*> binaryP) RJCT_ -> s (RJCT <$> A.takeByteString) @@ -1798,9 +1799,9 @@ parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX -- | Serialize SMP agent command. serializeCommand :: ACommand p e -> ByteString serializeCommand = \case - NEW ntfs cMode -> s (NEW_, ntfs, cMode) + NEW ntfs cMode subMode -> s (NEW_, ntfs, cMode, subMode) INV cReq -> s (INV_, cReq) - JOIN ntfs cReq cInfo -> s (JOIN_, ntfs, cReq, Str $ serializeBinary cInfo) + JOIN ntfs cReq subMode cInfo -> s (JOIN_, ntfs, cReq, subMode, Str $ serializeBinary cInfo) CONF confId srvs cInfo -> B.unwords [s CONF_, confId, strEncodeList srvs, serializeBinary cInfo] LET confId cInfo -> B.unwords [s LET_, confId, serializeBinary cInfo] REQ invId srvs cInfo -> B.unwords [s REQ_, invId, s srvs, serializeBinary cInfo] @@ -1905,7 +1906,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody APC e <$$> case cmd of SEND msgFlags body -> SEND msgFlags <$$> getBody body MSG msgMeta msgFlags body -> MSG msgMeta msgFlags <$$> getBody body - JOIN ntfs qUri cInfo -> JOIN ntfs qUri <$$> getBody cInfo + JOIN ntfs qUri subMode cInfo -> JOIN ntfs qUri subMode <$$> getBody cInfo CONF confId srvs cInfo -> CONF confId srvs <$$> getBody cInfo LET confId cInfo -> LET confId <$$> getBody cInfo REQ invId srvs cInfo -> REQ invId srvs <$$> getBody cInfo diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 571b9e741..5152c0212 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -495,9 +495,10 @@ createSMPQueue :: RcvPublicVerifyKey -> RcvPublicDhKey -> Maybe BasicAuth -> + SubscriptionMode -> ExceptT SMPClientError IO QueueIdsKeys -createSMPQueue c rpKey rKey dhKey auth = - sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth) >>= \case +createSMPQueue c rpKey rKey dhKey auth subMode = + sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case IDS qik -> pure qik r -> throwE . PCEUnexpectedResponse $ bshow r diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 2de573349..cd3c4ad64 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -47,6 +47,7 @@ module Simplex.Messaging.Protocol -- * SMP protocol types ProtocolEncoding (..), Command (..), + SubscriptionMode (..), Party (..), Cmd (..), BrokerMsg (..), @@ -154,7 +155,7 @@ import Control.Concurrent (threadDelay) import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J -import Data.Attoparsec.ByteString.Char8 (Parser) +import Data.Attoparsec.ByteString.Char8 (Parser, ()) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -276,7 +277,7 @@ type EntityId = ByteString -- | Parameterized type for SMP protocol commands from all clients. data Command (p :: Party) where -- SMP recipient commands - NEW :: RcvPublicVerifyKey -> RcvPublicDhKey -> Maybe BasicAuth -> Command Recipient + NEW :: RcvPublicVerifyKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> Command Recipient SUB :: Command Recipient KEY :: SndPublicVerifyKey -> Command Recipient NKEY :: NtfPublicVerifyKey -> RcvNtfPublicDhKey -> Command Recipient @@ -299,6 +300,26 @@ deriving instance Show (Command p) deriving instance Eq (Command p) +data SubscriptionMode = SMSubscribe | SMOnlyCreate + deriving (Eq, Show) + +instance StrEncoding SubscriptionMode where + strEncode = \case + SMSubscribe -> "subscribe" + SMOnlyCreate -> "only-create" + strP = + (A.string "subscribe" $> SMSubscribe) <|> (A.string "only-create" $> SMOnlyCreate) + "SubscriptionMode" + +instance Encoding SubscriptionMode where + smpEncode = \case + SMSubscribe -> "S" + SMOnlyCreate -> "C" + smpP = A.anyChar >>= \case + 'S' -> pure SMSubscribe + 'C' -> pure SMOnlyCreate + _ -> fail "bad SubscriptionMode" + data BrokerMsg where -- SMP broker messages (responses, client messages, notifications) IDS :: QueueIdsKeys -> BrokerMsg @@ -1044,13 +1065,13 @@ class ProtocolMsgTag (Tag msg) => ProtocolEncoding err msg | msg -> err where instance PartyI p => ProtocolEncoding ErrorType (Command p) where type Tag (Command p) = CommandTag p encodeProtocol v = \case - NEW rKey dhKey auth_ -> case auth_ of - Just auth - | v >= 5 -> new <> e ('A', auth) - | otherwise -> new - _ -> new + NEW rKey dhKey auth_ subMode + | v >= 6 -> new <> auth <> e subMode + | v == 5 -> new <> auth + | otherwise -> new where new = e (NEW_, ' ', rKey, dhKey) + auth = maybe "" (e . ('A',)) auth_ SUB -> e SUB_ KEY k -> e (KEY_, ' ', k) NKEY k dhKey -> e (NKEY_, ' ', k, dhKey) @@ -1102,10 +1123,12 @@ instance ProtocolEncoding ErrorType Cmd where CT SRecipient tag -> Cmd SRecipient <$> case tag of NEW_ - | v >= 5 -> new <*> optional (A.char 'A' *> smpP) - | otherwise -> new <*> pure Nothing + | v >= 6 -> new <*> auth <*> smpP + | v == 5 -> new <*> auth <*> pure SMSubscribe + | otherwise -> new <*> pure Nothing <*> pure SMSubscribe where new = NEW <$> _smpP <*> smpP + auth = optional (A.char 'A' *> smpP) SUB_ -> pure SUB KEY_ -> KEY <$> _smpP NKEY_ -> NKEY <$> _smpP <*> smpP @@ -1269,7 +1292,7 @@ tPutLog th s = do -- ByteString does not include length byte, it is added by tEncodeBatch data TransportBatch = TBTransmissions Int ByteString | TBTransmission ByteString | TBLargeTransmission --- | encodes and batches transmissions into blocks, +-- | encodes and batches transmissions into blocks, batchTransmissions :: Bool -> Int -> NonEmpty SentRawTransmission -> [TransportBatch] batchTransmissions batch bSize | batch = reverse . mkBatch [] . L.map tEncode diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 5651a883f..6ec234788 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -362,7 +362,7 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed verifyTransmission :: Maybe C.ASignature -> ByteString -> QueueId -> Cmd -> M VerificationResult verifyTransmission sig_ signed queueId cmd = case cmd of - Cmd SRecipient (NEW k _ _) -> pure $ Nothing `verified` verifyCmdSignature sig_ signed k + Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verified` verifyCmdSignature sig_ signed k Cmd SRecipient _ -> verifyCmd SRecipient $ verifyCmdSignature sig_ signed . recipientKey Cmd SSender SEND {} -> verifyCmd SSender $ verifyMaybe . senderKey Cmd SSender PING -> pure $ VRVerified Nothing @@ -422,10 +422,10 @@ client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Serv Cmd SNotifier NSUB -> subscribeNotifications Cmd SRecipient command -> case command of - NEW rKey dhKey auth -> + NEW rKey dhKey auth subMode -> ifM allowNew - (createQueue st rKey dhKey) + (createQueue st rKey dhKey subMode) (pure (corrId, queueId, ERR AUTH)) where allowNew = do @@ -440,8 +440,8 @@ client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Serv OFF -> suspendQueue_ st DEL -> delQueueAndMsgs st where - createQueue :: QueueStore -> RcvPublicVerifyKey -> RcvPublicDhKey -> m (Transmission BrokerMsg) - createQueue st recipientKey dhKey = time "NEW" $ do + createQueue :: QueueStore -> RcvPublicVerifyKey -> RcvPublicDhKey -> SubscriptionMode -> m (Transmission BrokerMsg) + createQueue st recipientKey dhKey subMode = time "NEW" $ do (rcvPublicDhKey, privDhKey) <- liftIO C.generateKeyPair' let rcvDhSecret = C.dh' dhKey privDhKey qik (rcvId, sndId) = QIK {rcvId, sndId, rcvPublicDhKey} @@ -472,7 +472,10 @@ client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Serv stats <- asks serverStats atomically $ modifyTVar' (qCreated stats) (+ 1) atomically $ modifyTVar' (qCount stats) (+ 1) - subscribeQueue qr rId $> IDS (qik ids) + case subMode of + SMOnlyCreate -> pure () + SMSubscribe -> void $ subscribeQueue qr rId + pure $ IDS (qik ids) logCreateById :: StoreLog 'WriteMode -> RecipientId -> IO () logCreateById s rId = diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 3c44e0989..4572a861e 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -98,7 +98,7 @@ smpBlockSize :: Int smpBlockSize = 16384 supportedSMPServerVRange :: VersionRange -supportedSMPServerVRange = mkVersionRange 1 5 +supportedSMPServerVRange = mkVersionRange 1 6 simplexMQVersion :: String simplexMQVersion = showVersion SMQ.version diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 2fc76494f..fbd2a54ed 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -21,6 +21,7 @@ import Control.Monad (forM_) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Type.Equality +import GHC.Stack (withFrozenCallStack) import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) @@ -123,12 +124,12 @@ h #: t = tPutRaw h t >> (<#:) h -- | action and expected response -- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r` (#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> AEntityTransmission 'Agent 'AEConn -> Expectation -action #> (corrId, connId, cmd) = action `shouldReturn` (corrId, connId, Right cmd) +action #> (corrId, connId, cmd) = withFrozenCallStack $ action `shouldReturn` (corrId, connId, Right cmd) -- | action and predicate for the response -- `h #:t =#> p` is the test that sends `t` to `h` and validates the response using `p` (=#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> (AEntityTransmission 'Agent 'AEConn -> Bool) -> Expectation -action =#> p = action >>= (`shouldSatisfy` p . correctTransmission) +action =#> p = withFrozenCallStack $ action >>= (`shouldSatisfy` p . correctTransmission) correctTransmission :: (ACorrId, ConnId, Either AgentErrorType cmd) -> (ACorrId, ConnId, cmd) correctTransmission (corrId, connId, cmdOrErr) = case cmdOrErr of @@ -163,9 +164,9 @@ pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} _ msgBody testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do - ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW T INV") + ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW T INV subscribe") let cReq' = strEncode cReq - bob #: ("11", "alice", "JOIN T " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) + bob #: ("11", "alice", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) ("", "bob", Right (CONF confId _ "bob's connInfo")) <- (alice <#:) alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) bob <# ("", "alice", INFO "alice's connInfo") @@ -196,9 +197,9 @@ testDuplexConnection _ alice bob = do testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do - ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW T INV") + ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW T INV subscribe") let cReq' = strEncode cReq - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> " 14\nbob's connInfo") + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") ("", bobConn', Right (CONF confId _ "bob's connInfo")) <- (alice <#:) bobConn' `shouldBe` bobConn alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False @@ -229,10 +230,10 @@ testDuplexConnRandomIds _ alice bob = do testContactConnection :: Transport c => TProxy c -> c -> c -> c -> IO () testContactConnection _ alice bob tom = do - ("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW T CON") + ("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW T CON subscribe") let cReq' = strEncode cReq - bob #: ("11", "alice", "JOIN T " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) + bob #: ("11", "alice", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) ("", "alice_contact", Right (REQ aInvId _ "bob's connInfo")) <- (alice <#:) alice #: ("2", "bob", "ACPT " <> aInvId <> " 16\nalice's connInfo") #> ("2", "bob", OK) ("", "alice", Right (CONF bConfId _ "alice's connInfo")) <- (bob <#:) @@ -245,7 +246,7 @@ testContactConnection _ alice bob tom = do bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False bob #: ("13", "alice", "ACK 4") #> ("13", "alice", OK) - tom #: ("21", "alice", "JOIN T " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK) + tom #: ("21", "alice", "JOIN T " <> cReq' <> " subscribe 14\ntom's connInfo") #> ("21", "alice", OK) ("", "alice_contact", Right (REQ aInvId' _ "tom's connInfo")) <- (alice <#:) alice #: ("4", "tom", "ACPT " <> aInvId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) ("", "alice", Right (CONF tConfId _ "alice's connInfo")) <- (tom <#:) @@ -260,10 +261,10 @@ testContactConnection _ alice bob tom = do testContactConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testContactConnRandomIds _ alice bob = do - ("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW T CON") + ("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW T CON subscribe") let cReq' = strEncode cReq - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> " 14\nbob's connInfo") + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") ("", aliceContact', Right (REQ aInvId _ "bob's connInfo")) <- (alice <#:) aliceContact' `shouldBe` aliceContact @@ -283,9 +284,9 @@ testContactConnRandomIds _ alice bob = do testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO () testRejectContactRequest _ alice bob = do - ("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW T CON") + ("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW T CON subscribe") let cReq' = strEncode cReq - bob #: ("11", "alice", "JOIN T " <> cReq' <> " 10\nbob's info") #> ("11", "alice", OK) + bob #: ("11", "alice", "JOIN T " <> cReq' <> " subscribe 10\nbob's info") #> ("11", "alice", OK) ("", "a_contact", Right (REQ aInvId _ "bob's info")) <- (alice <#:) -- RJCT must use correct contact connection alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND) @@ -314,7 +315,7 @@ testSubscription _ alice1 alice2 bob = do testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () testSubscrNotification t (server, _) client = do - client #: ("1", "conn1", "NEW T INV") =#> \case ("1", "conn1", INV {}) -> True; _ -> False + client #: ("1", "conn1", "NEW T INV subscribe") =#> \case ("1", "conn1", INV {}) -> True; _ -> False client #:# "nothing should be delivered to client before the server is killed" killThread server client <#. ("", "", DOWN testSMPServer ["conn1"]) @@ -424,9 +425,9 @@ testConcurrentMsgDelivery :: Transport c => TProxy c -> c -> c -> IO () testConcurrentMsgDelivery _ alice bob = do connect (alice, "alice") (bob, "bob") - ("1", "bob2", Right (INV cReq)) <- alice #: ("1", "bob2", "NEW T INV") + ("1", "bob2", Right (INV cReq)) <- alice #: ("1", "bob2", "NEW T INV subscribe") let cReq' = strEncode cReq - bob #: ("11", "alice2", "JOIN T " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice2", OK) + bob #: ("11", "alice2", "JOIN T " <> cReq' <> " subscribe 14\nbob's connInfo") #> ("11", "alice2", OK) ("", "bob2", Right (CONF _confId _ "bob's connInfo")) <- (alice <#:) -- below commands would be needed to accept bob's connection, but alice does not -- alice #: ("2", "bob", "LET " <> _confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) @@ -489,9 +490,9 @@ testResumeDeliveryQuotaExceeded _ alice bob = do connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do - ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW T INV") + ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW T INV subscribe") let cReq' = strEncode cReq - h2 #: ("c2", name1, "JOIN T " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK) + h2 #: ("c2", name1, "JOIN T " <> cReq' <> " subscribe 5\ninfo2") #> ("c2", name1, OK) ("", _, Right (CONF connId _ "info2")) <- (h1 <#:) h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) h2 <# ("", name1, INFO "info1") @@ -510,9 +511,9 @@ sendMessage (h1, name1) (h2, name2) msg = do -- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) -- connect' h1 h2 = do --- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW T INV") +-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW T INV subscribe") -- let cReq' = strEncode cReq --- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN T " <> cReq' <> " 5\ninfo2") +-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN T " <> cReq' <> " subscribe 5\ninfo2") -- ("", _, Right (REQ connId _ "info2")) <- (h1 <#:) -- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False -- h2 <# ("", conn1, INFO "info1") @@ -528,9 +529,9 @@ syntaxTests t = do it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX") describe "NEW" $ do describe "valid" $ do - it "with correct parameter" $ ("211", "", "NEW T INV") >#>= \case ("211", _, "INV" : _) -> True; _ -> False + it "with correct parameter" $ ("211", "", "NEW T INV subscribe") >#>= \case ("211", _, "INV" : _) -> True; _ -> False describe "invalid" $ do - it "with incorrect parameter" $ ("222", "", "NEW T hi") >#> ("222", "", "ERR CMD SYNTAX") + it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX") describe "JOIN" $ do describe "valid" $ do @@ -543,7 +544,8 @@ syntaxTests t = do <> urlEncode True sampleDhKey <> "&v=1" <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - <> " 14\nbob's connInfo" + <> " subscribe " + <> "14\nbob's connInfo" ) >#> ("311", "a", "ERR SMP AUTH") describe "invalid" $ do @@ -551,8 +553,8 @@ syntaxTests t = do where -- simple test for one command with the expected response (>#>) :: ARawTransmission -> ARawTransmission -> Expectation - command >#> response = smpAgentTest t command `shouldReturn` response + command >#> response = withFrozenCallStack $ smpAgentTest t command `shouldReturn` response -- simple test for one command with a predicate for the expected response (>#>=) :: ARawTransmission -> ((ByteString, ByteString, [ByteString]) -> Bool) -> Expectation - command >#>= p = smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, connId, cmd) -> (cId, connId, B.words cmd)) + command >#>= p = withFrozenCallStack $ smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, connId, cmd) -> (cId, connId, B.words cmd)) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 6b0acbe1e..7f50e3591 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} @@ -54,7 +55,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolServer (..), supportedSMPClientVRange) +import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolServer (..), SubscriptionMode (..), supportedSMPClientVRange) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration @@ -68,11 +69,17 @@ import XFTPClient (testXFTPServer) type AEntityTransmission e = (ACorrId, ConnId, ACommand 'Agent e) -(##>) :: (HasCallStack, MonadIO m) => m (AEntityTransmission e) -> AEntityTransmission e -> m () -a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t) +(##>) :: (HasCallStack, MonadUnliftIO m) => m (AEntityTransmission e) -> AEntityTransmission e -> m () +a ##> t = withTimeout a (`shouldBe` t) -(=##>) :: (Show a, HasCallStack, MonadIO m) => m a -> (a -> Bool) -> m () -a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p) +(=##>) :: (Show a, HasCallStack, MonadUnliftIO m) => m a -> (a -> Bool) -> m () +a =##> p = withTimeout a (`shouldSatisfy` p) + +withTimeout :: MonadUnliftIO m => m a -> (a -> Expectation) -> m () +withTimeout a test = + timeout 10_000000 a >>= \case + Nothing -> error "operation timed out" + Just t -> liftIO $ test t get :: MonadIO m => AgentClient -> m (AEntityTransmission 'AEConn) get = get' @'AEConn @@ -204,6 +211,9 @@ functionalAPITests t = do testRatchetSyncSuspendForeground t it "should synchronize ratchets when clients start synchronization simultaneously" $ testRatchetSyncSimultaneous t + describe "Subscription mode OnlyCreate" $ do + it "messages delivered only when polled" $ + withSmpServer t testOnlyCreatePull describe "Inactive client disconnection" $ do it "should disconnect clients if it was inactive longer than TTL" $ testInactiveClientDisconnected t @@ -356,8 +366,8 @@ withAgentClients2 = withAgentClientsCfg2 agentCfg agentCfg runAgentClientTest :: HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO () runAgentClientTest alice bob baseId = do runRight_ $ do - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) @@ -391,10 +401,10 @@ runAgentClientTest alice bob baseId = do runAgentClientContactTest :: HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO () runAgentClientContactTest alice bob baseId = do runRight_ $ do - (_, qInfo) <- createConnection alice 1 True SCMContact Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (_, qInfo) <- createConnection alice 1 True SCMContact Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, REQ invId _ "bob's connInfo") <- get alice - bobId <- acceptContact alice True invId "alice's connInfo" + bobId <- acceptContact alice True invId "alice's connInfo" SMSubscribe ("", _, CONF confId _ "alice's connInfo") <- get bob allowConnection bob aliceId confId "bob's connInfo" get alice ##> ("", bobId, INFO "bob's connInfo") @@ -436,9 +446,9 @@ noMessages c err = tryGet `shouldReturn` () testAsyncInitiatingOffline :: HasCallStack => IO () testAsyncInitiatingOffline = withAgentClients2 $ \alice bob -> runRight_ $ do - (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing + (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe disconnectAgentClient alice - aliceId <- joinConnection bob 1 True cReq "bob's connInfo" + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe alice' <- liftIO $ getSMPAgentClient' agentCfg initAgentServers testDB subscribeConnection alice' bobId ("", _, CONF confId _ "bob's connInfo") <- get alice' @@ -451,8 +461,8 @@ testAsyncInitiatingOffline = testAsyncJoiningOfflineBeforeActivation :: HasCallStack => IO () testAsyncJoiningOfflineBeforeActivation = withAgentClients2 $ \alice bob -> runRight_ $ do - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe disconnectAgentClient bob ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" @@ -466,9 +476,9 @@ testAsyncJoiningOfflineBeforeActivation = testAsyncBothOffline :: HasCallStack => IO () testAsyncBothOffline = withAgentClients2 $ \alice bob -> runRight_ $ do - (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing + (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe disconnectAgentClient alice - aliceId <- joinConnection bob 1 True cReq "bob's connInfo" + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe disconnectAgentClient bob alice' <- liftIO $ getSMPAgentClient' agentCfg initAgentServers testDB subscribeConnection alice' bobId @@ -485,9 +495,9 @@ testAsyncServerOffline :: HasCallStack => ATransport -> IO () testAsyncServerOffline t = withAgentClients2 $ \alice bob -> do -- create connection and shutdown the server (bobId, cReq) <- withSmpServerStoreLogOn t testPort $ \_ -> - runRight $ createConnection alice 1 True SCMInvitation Nothing + runRight $ createConnection alice 1 True SCMInvitation Nothing SMSubscribe -- connection fails - Left (BROKER _ NETWORK) <- runExceptT $ joinConnection bob 1 True cReq "bob's connInfo" + Left (BROKER _ NETWORK) <- runExceptT $ joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe ("", "", DOWN srv conns) <- nGet alice srv `shouldBe` testSMPServer conns `shouldBe` [bobId] @@ -497,7 +507,7 @@ testAsyncServerOffline t = withAgentClients2 $ \alice bob -> do liftIO $ do srv1 `shouldBe` testSMPServer conns1 `shouldBe` [bobId] - aliceId <- joinConnection bob 1 True cReq "bob's connInfo" + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) @@ -512,9 +522,9 @@ testAsyncHelloTimeout = do smpCfgV1 = (smpCfg agentCfg) {serverVRange = vr11} agentCfgV1 = agentCfg {smpAgentVRange = vr11, smpClientVRange = vr11, e2eEncryptVRange = vr11, smpCfg = smpCfgV1} withAgentClientsCfg2 agentCfgV1 agentCfg {helloTimeout = 1} $ \alice bob -> runRight_ $ do - (_, cReq) <- createConnection alice 1 True SCMInvitation Nothing + (_, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe disconnectAgentClient alice - aliceId <- joinConnection bob 1 True cReq "bob's connInfo" + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe get bob ##> ("", aliceId, ERR $ CONN NOT_ACCEPTED) testAllowConnectionClientRestart :: HasCallStack => ATransport -> IO () @@ -526,8 +536,8 @@ testAllowConnectionClientRestart t = do (aliceId, bobId, confId) <- withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile2} testPort2 $ \_ -> do runRight $ do - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice pure (aliceId, bobId, confId) @@ -995,13 +1005,46 @@ testRatchetSyncSimultaneous t = do disconnectAgentClient bob disconnectAgentClient bob2 +testOnlyCreatePull :: IO () +testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMOnlyCreate + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMOnlyCreate + getMsg alice bobId + Just ("", _, CONF confId _ "bob's connInfo") <- timeout 5_000000 $ get alice + allowConnection alice bobId confId "alice's connInfo" + liftIO $ threadDelay 1_000000 + getMsg bob aliceId + get bob ##> ("", aliceId, INFO "alice's connInfo") + liftIO $ threadDelay 1_000000 + getMsg alice bobId + get alice ##> ("", bobId, CON) + getMsg bob aliceId + get bob ##> ("", aliceId, CON) + -- exchange messages + 4 <- sendMessage alice bobId SMP.noMsgFlags "hello" + get alice ##> ("", bobId, SENT 4) + getMsg bob aliceId + get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + ackMessage bob aliceId 4 Nothing + 5 <- sendMessage bob aliceId SMP.noMsgFlags "hello too" + get bob ##> ("", aliceId, SENT 5) + getMsg alice bobId + get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + ackMessage alice bobId 5 Nothing + where + getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO () + getMsg c cId = do + liftIO $ noMessages c "nothing should be delivered before GET" + Just _ <- getConnectionMessage c cId + pure () + makeConnection :: AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnection alice bob = makeConnectionForUsers alice 1 bob 1 makeConnectionForUsers :: AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers alice aliceUserId bob bobUserId = do - (bobId, qInfo) <- createConnection alice aliceUserId True SCMInvitation Nothing - aliceId <- joinConnection bob bobUserId True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice aliceUserId True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob bobUserId True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) @@ -1015,7 +1058,7 @@ testInactiveClientDisconnected t = do withSmpServerConfigOn t cfg' testPort $ \_ -> do alice <- getSMPAgentClient' agentCfg initAgentServers testDB runRight_ $ do - (connId, _cReq) <- createConnection alice 1 True SCMInvitation Nothing + (connId, _cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe nGet alice ##> ("", "", DOWN testSMPServer [connId]) disconnectAgentClient alice @@ -1026,7 +1069,7 @@ testActiveClientNotDisconnected t = do alice <- getSMPAgentClient' agentCfg initAgentServers testDB ts <- getSystemTime runRight_ $ do - (connId, _cReq) <- createConnection alice 1 True SCMInvitation Nothing + (connId, _cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe keepSubscribing alice connId ts disconnectAgentClient alice where @@ -1181,10 +1224,10 @@ testBatchedSubscriptions nCreate nDel t = do testAsyncCommands :: IO () testAsyncCommands = withAgentClients2 $ \alice bob -> runRight_ $ do - bobId <- createConnectionAsync alice 1 "1" True SCMInvitation + bobId <- createConnectionAsync alice 1 "1" True SCMInvitation SMSubscribe ("1", bobId', INV (ACR _ qInfo)) <- get alice liftIO $ bobId' `shouldBe` bobId - aliceId <- joinConnectionAsync bob 1 "2" True qInfo "bob's connInfo" + aliceId <- joinConnectionAsync bob 1 "2" True qInfo "bob's connInfo" SMSubscribe ("2", aliceId', OK) <- get bob liftIO $ aliceId' `shouldBe` aliceId ("", _, CONF confId _ "bob's connInfo") <- get alice @@ -1225,7 +1268,7 @@ testAsyncCommands = testAsyncCommandsRestore :: ATransport -> IO () testAsyncCommandsRestore t = do alice <- getSMPAgentClient' agentCfg initAgentServers testDB - bobId <- runRight $ createConnectionAsync alice 1 "1" True SCMInvitation + bobId <- runRight $ createConnectionAsync alice 1 "1" True SCMInvitation SMSubscribe liftIO $ noMessages alice "alice doesn't receive INV because server is down" disconnectAgentClient alice alice' <- liftIO $ getSMPAgentClient' agentCfg initAgentServers testDB @@ -1239,10 +1282,10 @@ testAsyncCommandsRestore t = do testAcceptContactAsync :: IO () testAcceptContactAsync = withAgentClients2 $ \alice bob -> runRight_ $ do - (_, qInfo) <- createConnection alice 1 True SCMContact Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (_, qInfo) <- createConnection alice 1 True SCMContact Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, REQ invId _ "bob's connInfo") <- get alice - bobId <- acceptContactAsync alice "1" True invId "alice's connInfo" + bobId <- acceptContactAsync alice "1" True invId "alice's connInfo" SMSubscribe ("1", bobId', OK) <- get alice liftIO $ bobId' `shouldBe` bobId ("", _, CONF confId _ "alice's connInfo") <- get bob @@ -1280,9 +1323,9 @@ testDeleteConnectionAsync :: ATransport -> IO () testDeleteConnectionAsync t = do a <- getSMPAgentClient' agentCfg {initialCleanupDelay = 10000, cleanupInterval = 10000, deleteErrorCount = 3} initAgentServers testDB connIds <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do - (bId1, _inv) <- createConnection a 1 True SCMInvitation Nothing - (bId2, _inv) <- createConnection a 1 True SCMInvitation Nothing - (bId3, _inv) <- createConnection a 1 True SCMInvitation Nothing + (bId1, _inv) <- createConnection a 1 True SCMInvitation Nothing SMSubscribe + (bId2, _inv) <- createConnection a 1 True SCMInvitation Nothing SMSubscribe + (bId3, _inv) <- createConnection a 1 True SCMInvitation Nothing SMSubscribe pure ([bId1, bId2, bId3] :: [ConnId]) runRight_ $ do deleteConnectionsAsync a connIds @@ -1301,10 +1344,10 @@ testJoinConnectionAsyncReplyError t = do a <- getSMPAgentClient' agentCfg initAgentServers testDB b <- getSMPAgentClient' agentCfg initAgentServersSrv2 testDB2 (aId, bId) <- withSmpServerStoreLogOn t testPort $ \_ -> runRight $ do - bId <- createConnectionAsync a 1 "1" True SCMInvitation + bId <- createConnectionAsync a 1 "1" True SCMInvitation SMSubscribe ("1", bId', INV (ACR _ qInfo)) <- get a liftIO $ bId' `shouldBe` bId - aId <- joinConnectionAsync b 1 "2" True qInfo "bob's connInfo" + aId <- joinConnectionAsync b 1 "2" True qInfo "bob's connInfo" SMSubscribe liftIO $ threadDelay 500000 ConnectionStats {rcvQueuesInfo = [], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId pure (aId, bId) @@ -1781,11 +1824,11 @@ testCreateQueueAuth clnt1 clnt2 = do a <- getClient clnt1 b <- getClient clnt2 r <- runRight $ do - tryError (createConnection a 1 True SCMInvitation Nothing) >>= \case + tryError (createConnection a 1 True SCMInvitation Nothing SMSubscribe) >>= \case Left (SMP AUTH) -> pure 0 Left e -> throwError e Right (bId, qInfo) -> - tryError (joinConnection b 1 True qInfo "bob's connInfo") >>= \case + tryError (joinConnection b 1 True qInfo "bob's connInfo" SMSubscribe) >>= \case Left (SMP AUTH) -> pure 1 Left e -> throwError e Right aId -> do @@ -1960,8 +2003,8 @@ getSMPAgentClient' cfg' initServers dbPath = do testServerMultipleIdentities :: HasCallStack => IO () testServerMultipleIdentities = withAgentClients2 $ \alice bob -> runRight_ $ do - (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True cReq "bob's connInfo" + (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) @@ -1969,7 +2012,7 @@ testServerMultipleIdentities = get bob ##> ("", aliceId, CON) exchangeGreetings alice bobId bob aliceId -- this saves queue with second server identity - Left (BROKER _ NETWORK) <- runExceptT $ joinConnection bob 1 True secondIdentityCReq "bob's connInfo" + Left (BROKER _ NETWORK) <- runExceptT $ joinConnection bob 1 True secondIdentityCReq "bob's connInfo" SMSubscribe disconnectAgentClient bob bob' <- liftIO $ getSMPAgentClient' agentCfg initAgentServers testDB2 subscribeConnection bob' aliceId diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index f1e6ec842..d5e9db960 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -29,7 +29,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) -import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), SMPMsgMeta (..)) +import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), SMPMsgMeta (..), SubscriptionMode (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Transport (ATransport) @@ -217,8 +217,8 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} = do bob <- getSMPAgentClient' agentCfg initAgentServers testDB2 (bobId, aliceId, nonce, message) <- runRight $ do -- establish connection - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get bob ##> ("", aliceId, INFO "alice's connInfo") @@ -280,9 +280,9 @@ testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} = do DeviceToken {} <- registerTestToken bob "bcde" NMInstant apnsQ -- establish connection liftIO $ threadDelay 50000 - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe liftIO $ threadDelay 1000000 - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe liftIO $ threadDelay 750000 void $ messageNotification apnsQ ("", _, CONF confId _ "bob's connInfo") <- get alice @@ -333,8 +333,8 @@ testChangeNotificationsMode APNSMockServer {apnsQ} = do bob <- getSMPAgentClient' agentCfg initAgentServers testDB2 runRight_ $ do -- establish connection - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get bob ##> ("", aliceId, INFO "alice's connInfo") @@ -399,8 +399,8 @@ testChangeToken APNSMockServer {apnsQ} = do bob <- getSMPAgentClient' agentCfg initAgentServers testDB2 (aliceId, bobId) <- runRight $ do -- establish connection - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get bob ##> ("", aliceId, INFO "alice's connInfo") diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 0fa10ae4e..8a56be9b5 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -22,6 +22,7 @@ import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.Set as S +import GHC.Stack (withFrozenCallStack) import SMPClient import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -121,7 +122,7 @@ testCreateSecureV2 _ = withSmpServerConfigOn (transport @c) cfgV2 testPort $ \_ -> testSMPClient @c $ \h -> do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd448 (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dec = decryptMsgV2 $ C.dh' srvDh dhPriv (rId1, "") #== "creates queue" @@ -185,7 +186,7 @@ testCreateSecure (ATransport t) = smpTest2 t $ \r s -> do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd448 (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv (rId1, "") #== "creates queue" @@ -249,7 +250,7 @@ testCreateDelete (ATransport t) = smpTest2 t $ \rh sh -> do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd25519 (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv (rId1, "") #== "creates queue" @@ -320,7 +321,7 @@ stressTest (ATransport t) = (rPub, rKey) <- C.generateSignatureKeyPair C.SEd25519 (dhPub, _ :: C.PrivateKeyX25519) <- C.generateKeyPair' rIds <- forM ([1 .. 50] :: [Int]) . const $ do - Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub Nothing) + Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub Nothing SMSubscribe) pure rId let subscribeQueues h = forM_ rIds $ \rId -> do Resp "" rId' OK <- signSendRecv h rKey ("", rId, SUB) @@ -337,7 +338,7 @@ testAllowNewQueues t = testSMPClient @c $ \h -> do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd448 (dhPub, _ :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) pure () testDuplex :: ATransport -> Spec @@ -346,7 +347,7 @@ testDuplex (ATransport t) = smpTest2 t $ \alice bob -> do (arPub, arKey) <- C.generateSignatureKeyPair C.SEd448 (aDhPub, aDhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub Nothing) + Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub Nothing SMSubscribe) let aDec = decryptMsgV3 $ C.dh' aSrvDh aDhPriv -- aSnd ID is passed to Bob out-of-band @@ -362,7 +363,7 @@ testDuplex (ATransport t) = (brPub, brKey) <- C.generateSignatureKeyPair C.SEd448 (bDhPub, bDhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub Nothing) + Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub Nothing SMSubscribe) let bDec = decryptMsgV3 $ C.dh' bSrvDh bDhPriv Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, _SEND $ "reply_id " <> encode bSnd) -- "reply_id ..." is ad-hoc, not a part of SMP protocol @@ -400,7 +401,7 @@ testSwitchSub (ATransport t) = smpTest3 t $ \rh1 rh2 sh -> do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd448 (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, _SEND "test1") (ok1, OK) #== "sent test message 1" @@ -845,7 +846,7 @@ createAndSecureQueue :: Transport c => THandle c -> SndPublicVerifyKey -> IO (Se createAndSecureQueue h sPub = do (rPub, rKey) <- C.generateSignatureKeyPair C.SEd448 (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dhShared = C.dh' srvDh dhPriv Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub) (rId', rId) #== "same queue ID" @@ -870,7 +871,7 @@ testTiming (ATransport t) = testSameTiming rh sh (goodKeySize, badKeySize, n) = do (rPub, rKey) <- generateKeys goodKeySize (dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair' - Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing) + Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe) let dec = decryptMsgV3 $ C.dh' srvDh dhPriv Resp "cdab" _ OK <- signSendRecv rh rKey ("cdab", rId, SUB) @@ -1007,14 +1008,17 @@ sampleDhPubKey = "MCowBQYDK2VuAyEAriy+HcARIhqsgSjVnjKqoft+y6pxrxdY68zn4+LjYhQ=" sampleSig :: Maybe C.ASignature sampleSig = "e8JK+8V3fq6kOLqco/SaKlpNaQ7i1gfOrXoqekEl42u4mF8Bgu14T5j0189CGcUhJHw2RwCMvON+qbvQ9ecJAA==" +noAuth :: (Char, Maybe BasicAuth) +noAuth = ('A', Nothing) + syntaxTests :: ATransport -> Spec syntaxTests (ATransport t) = do it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN) describe "NEW" $ do it "no parameters" $ (sampleSig, "bcda", "", NEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX) it "many parameters" $ (sampleSig, "cdab", "", (NEW_, ' ', ('\x01', 'A'), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX) - it "no signature" $ ("", "dabc", "", (NEW_, ' ', samplePubKey, sampleDhPubKey)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) - it "queue ID" $ (sampleSig, "abcd", "12345678", (NEW_, ' ', samplePubKey, sampleDhPubKey)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) + it "no signature" $ ("", "dabc", "", (NEW_, ' ', samplePubKey, sampleDhPubKey, SMSubscribe)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) + it "queue ID" $ (sampleSig, "abcd", "12345678", (NEW_, ' ', samplePubKey, sampleDhPubKey, SMSubscribe)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) describe "KEY" $ do it "valid syntax" $ (sampleSig, "bcda", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "12345678", ERR AUTH) it "no parameters" $ (sampleSig, "cdab", "12345678", KEY_) >#> ("", "cdab", "12345678", ERR $ CMD SYNTAX) @@ -1049,4 +1053,4 @@ syntaxTests (ATransport t) = do (Maybe C.ASignature, ByteString, ByteString, smp) -> (Maybe C.ASignature, ByteString, ByteString, BrokerMsg) -> Expectation - command >#> response = smpServerTest t command `shouldReturn` response + command >#> response = withFrozenCallStack $ smpServerTest t command `shouldReturn` response