From ab89963f45fe81eb7355356e6823f8a1ff545840 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 11 Jun 2021 21:33:13 +0100 Subject: [PATCH 01/29] introduction protocol (#156) * commands to support introduction * agent messages / envelopes to support introductions * introductions and invitations table; insert record with random unique ID * store class methods and types for introductions * process INTRO and ACPT commands for connection introductions * fix tests: add MonadFail constraint, remove OK response to JOIN * process agent messages for introductions * ICON notification when introduction is completed * replace multiway if with case * correction * support random connection IDs * save additional connection fields, refactor create connection funcs * refactor * refactor * test duplex connection with random IDs * store methods for introductions * test introduction * fix parsing of CON agent message * test introduction with random connection IDs * broadcast with random connection and broadcast IDs * clean up sql --- migrations/20210529_broadcasts.sql | 7 +- migrations/20210602_introductions.sql | 27 + src/Simplex/Messaging/Agent.hs | 241 ++++++-- src/Simplex/Messaging/Agent/Client.hs | 27 +- src/Simplex/Messaging/Agent/Protocol.hs | 124 +++- src/Simplex/Messaging/Agent/Store.hs | 141 ++++- src/Simplex/Messaging/Agent/Store/SQLite.hs | 598 ++++++++++++++------ src/Simplex/Messaging/Parsers.hs | 11 +- tests/AgentTests.hs | 188 +++++- tests/AgentTests/SQLiteTests.hs | 191 ++++--- tests/SMPAgentClient.hs | 14 +- 11 files changed, 1156 insertions(+), 413 deletions(-) create mode 100644 migrations/20210602_introductions.sql diff --git a/migrations/20210529_broadcasts.sql b/migrations/20210529_broadcasts.sql index 3095f0572..91cba0eb6 100644 --- a/migrations/20210529_broadcasts.sql +++ b/migrations/20210529_broadcasts.sql @@ -1,9 +1,8 @@ -CREATE TABLE IF NOT EXISTS broadcasts ( - broadcast_id BLOB NOT NULL, - PRIMARY KEY (broadcast_id) +CREATE TABLE broadcasts ( + broadcast_id BLOB NOT NULL PRIMARY KEY ) WITHOUT ROWID; -CREATE TABLE IF NOT EXISTS broadcast_connections ( +CREATE TABLE broadcast_connections ( broadcast_id BLOB NOT NULL REFERENCES broadcasts (broadcast_id) ON DELETE CASCADE, conn_alias BLOB NOT NULL REFERENCES connections (conn_alias), PRIMARY KEY (broadcast_id, conn_alias) diff --git a/migrations/20210602_introductions.sql b/migrations/20210602_introductions.sql new file mode 100644 index 000000000..d382b2961 --- /dev/null +++ b/migrations/20210602_introductions.sql @@ -0,0 +1,27 @@ +CREATE TABLE conn_intros ( + intro_id BLOB NOT NULL PRIMARY KEY, + to_conn BLOB NOT NULL REFERENCES connections (conn_alias) ON DELETE CASCADE, + to_info BLOB, -- info about "to" connection sent to "re" connection + to_status TEXT NOT NULL DEFAULT '', -- '', INV, CON + re_conn BLOB NOT NULL REFERENCES connections (conn_alias) ON DELETE CASCADE, + re_info BLOB NOT NULL, -- info about "re" connection sent to "to" connection + re_status TEXT NOT NULL DEFAULT '', -- '', INV, CON + queue_info BLOB +) WITHOUT ROWID; + +CREATE TABLE conn_invitations ( + inv_id BLOB NOT NULL PRIMARY KEY, + via_conn BLOB REFERENCES connections (conn_alias) ON DELETE SET NULL, + external_intro_id BLOB NOT NULL, + conn_info BLOB, -- info about another connection + queue_info BLOB, -- NULL if it's an initial introduction + conn_id BLOB REFERENCES connections (conn_alias) -- created connection + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + status TEXT DEFAULT '' -- '', 'ACPT', 'CON' +) WITHOUT ROWID; + +ALTER TABLE connections + ADD via_inv BLOB REFERENCES conn_invitations (inv_id) ON DELETE RESTRICT; +ALTER TABLE connections + ADD conn_level INTEGER DEFAULT 0; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 25bc15c91..e29ea41b0 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -61,7 +61,7 @@ import UnliftIO.STM -- | Runs an SMP agent as a TCP service using passed configuration. -- -- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs -runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m () +runSMPAgent :: (MonadFail m, MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m () runSMPAgent t cfg = do started <- newEmptyTMVarIO runSMPAgentBlocking t started cfg @@ -70,10 +70,10 @@ runSMPAgent t cfg = do -- -- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True) -- and when it is disconnected from the TCP socket once the server thread is killed (False). -runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m () +runSMPAgentBlocking :: (MonadFail m, MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m () runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReaderT (smpAgent t) =<< newSMPAgentEnv cfg where - smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' () + smpAgent :: forall c m'. (Transport c, MonadFail m', MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' () smpAgent _ = runTransportServer started tcpPort $ \(h :: c) -> do liftIO $ putLn h "Welcome to SMP v0.3.2 agent" c <- getSMPAgentClient @@ -97,7 +97,7 @@ logConnection c connected = in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"] -- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. -runSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () +runSMPAgentClient :: (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () runSMPAgentClient c = do db <- asks $ dbFile . config s1 <- liftIO $ connectSQLiteStore db @@ -128,7 +128,7 @@ logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> logClient AgentClient {clientId} dir (ATransmission corrId entity cmd) = do logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, serializeEntity entity, B.takeWhile (/= ' ') $ serializeCommand cmd] -client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () +client :: forall m. (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () client c@AgentClient {rcvQ, sndQ} st = forever loop where loop :: m () @@ -147,6 +147,8 @@ withStore action = do Right c -> return c Left e -> throwError $ storeError e where + -- TODO when parsing exception happens in store, the agent hangs; + -- changing SQLError to SomeException does not help handleInternal :: (MonadError StoreError m') => SQLError -> m' a handleInternal e = throwError . SEInternal $ bshow e storeError :: StoreError -> AgentErrorType @@ -173,23 +175,28 @@ unsupportedEntity c _ corrId entity _ = processConnCommand :: forall c m. (AgentMonad m, EntityCommand 'Conn_ c) => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Conn_ -> ACommand 'Client c -> m () -processConnCommand c@AgentClient {sndQ} st corrId conn = \case - NEW -> createNewConnection conn - JOIN smpQueueInfo replyMode -> joinConnection conn smpQueueInfo replyMode +processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case + NEW -> createNewConnection Nothing 0 >>= uncurry respond + JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode Nothing 0 >> pure () -- >>= (`respond` OK) + INTRO reEntity reInfo -> makeIntroduction reEntity reInfo + ACPT inv eInfo -> acceptInvitation inv eInfo SUB -> subscribeConnection conn SUBALL -> subscribeAll - SEND msgBody -> sendMessage c st corrId conn msgBody + SEND msgBody -> sendClientMessage c st corrId conn msgBody OFF -> suspendConnection conn DEL -> deleteConnection conn where - createNewConnection :: Entity 'Conn_ -> m () - createNewConnection (Conn cId) = do + createNewConnection :: Maybe InvitationId -> Int -> m (Entity 'Conn_, ACommand 'Agent 'INV_) + createNewConnection viaInv connLevel = do -- TODO create connection alias if not passed - -- make connAlias Maybe? + -- make connId Maybe? srv <- getSMPServer - (rq, qInfo) <- newReceiveQueue c srv cId - withStore $ createRcvConn st rq - respond conn $ INV qInfo + (rq, qInfo) <- newReceiveQueue c srv + g <- asks idsDrg + let cData = ConnData {connId, viaInv, connLevel} + connId' <- withStore $ createRcvConn st g cData rq + addSubscription c rq connId' + pure (Conn connId', INV qInfo) getSMPServer :: m SMPServer getSMPServer = @@ -200,16 +207,47 @@ processConnCommand c@AgentClient {sndQ} st corrId conn = \case i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1) pure $ servers L.!! i - joinConnection :: Entity 'Conn_ -> SMPQueueInfo -> ReplyMode -> m () - joinConnection (Conn cId) qInfo (ReplyMode replyMode) = do - -- TODO create connection alias if not passed - -- make connAlias Maybe? - (sq, senderKey, verifyKey) <- newSendQueue qInfo cId - withStore $ createSndConn st sq + joinConnection :: SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m (Entity 'Conn_) + joinConnection qInfo (ReplyMode replyMode) viaInv connLevel = do + (sq, senderKey, verifyKey) <- newSendQueue qInfo + g <- asks idsDrg + let cData = ConnData {connId, viaInv, connLevel} + connId' <- withStore $ createSndConn st g cData sq connectToSendQueue c st sq senderKey verifyKey - when (replyMode == On) $ createReplyQueue cId sq - -- TODO this response is disabled to avoid two responses in terminal client (OK + CON), - -- respond conn OK + when (replyMode == On) $ createReplyQueue connId' sq + pure $ Conn connId' + + makeIntroduction :: IntroEntity -> EntityInfo -> m () + makeIntroduction (IE reEntity) reInfo = case reEntity of + Conn reConn -> + withStore ((,) <$> getConn st connId <*> getConn st reConn) >>= \case + (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do + g <- asks idsDrg + introId <- withStore $ createIntro st g NewIntroduction {toConn = connId, reConn, reInfo} + sendControlMessage c sq $ A_INTRO (IE (Conn introId)) reInfo + respond conn OK + _ -> throwError $ CONN SIMPLEX + _ -> throwError $ CMD UNSUPPORTED + + acceptInvitation :: IntroEntity -> EntityInfo -> m () + acceptInvitation (IE invEntity) eInfo = case invEntity of + Conn invId -> do + withStore (getInvitation st invId) >>= \case + Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> + withStore (getConn st viaConn) >>= \case + SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of + Nothing -> do + (conn', INV qInfo') <- createNewConnection (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId $ fromConn conn' + sendControlMessage c sq $ A_INV (Conn externalIntroId) qInfo' eInfo + respond conn' OK + Just qInfo' -> do + conn' <- joinConnection qInfo' (ReplyMode On) (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId $ fromConn conn' + respond conn' OK + _ -> throwError $ CONN SIMPLEX + _ -> throwError $ CMD PROHIBITED + _ -> throwError $ CMD UNSUPPORTED subscribeConnection :: Entity 'Conn_ -> m () subscribeConnection conn'@(Conn cId) = @@ -222,7 +260,7 @@ processConnCommand c@AgentClient {sndQ} st corrId conn = \case -- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct subscribeAll :: m () - subscribeAll = withStore (getAllConnAliases st) >>= mapM_ (subscribeConnection . Conn) + subscribeAll = withStore (getAllConnIds st) >>= mapM_ (subscribeConnection . Conn) suspendConnection :: Entity 'Conn_ -> m () suspendConnection (Conn cId) = @@ -246,25 +284,30 @@ processConnCommand c@AgentClient {sndQ} st corrId conn = \case removeSubscription c cId delConn - createReplyQueue :: ByteString -> SndQueue -> m () + createReplyQueue :: ConnId -> SndQueue -> m () createReplyQueue cId sq = do srv <- getSMPServer - (rq, qInfo) <- newReceiveQueue c srv cId + (rq, qInfo) <- newReceiveQueue c srv + addSubscription c rq cId withStore $ upgradeSndConnToDuplex st cId rq - senderTimestamp <- liftIO getCurrentTime - sendAgentMessage c sq . serializeSMPMessage $ - SMPMessage - { senderMsgId = 0, - senderTimestamp, - previousMsgHash = "", - agentMessage = REPLY qInfo - } + sendControlMessage c sq $ REPLY qInfo respond :: EntityCommand t c' => Entity t -> ACommand 'Agent c' -> m () respond ent resp = atomically . writeTBQueue sndQ $ ATransmission corrId ent resp -sendMessage :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Conn_ -> MsgBody -> m () -sendMessage c st corrId (Conn cId) msgBody = +sendControlMessage :: AgentMonad m => AgentClient -> SndQueue -> AMessage -> m () +sendControlMessage c sq agentMessage = do + senderTimestamp <- liftIO getCurrentTime + sendAgentMessage c sq . serializeSMPMessage $ + SMPMessage + { senderMsgId = 0, + senderTimestamp, + previousMsgHash = "", + agentMessage + } + +sendClientMessage :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Conn_ -> MsgBody -> m () +sendClientMessage c st corrId (Conn cId) msgBody = withStore (getConn st cId) >>= \case SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq SomeConn _ (SndConnection _ sq) -> sendMsg sq @@ -273,7 +316,7 @@ sendMessage c st corrId (Conn cId) msgBody = sendMsg :: SndQueue -> m () sendMsg sq = do internalTs <- liftIO getCurrentTime - (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st sq + (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st cId let msgStr = serializeSMPMessage SMPMessage @@ -284,7 +327,7 @@ sendMessage c st corrId (Conn cId) msgBody = } msgHash = C.sha256Hash msgStr withStore $ - createSndMsg st sq $ + createSndMsg st cId $ SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} sendAgentMessage c sq msgStr atomically . writeTBQueue (sndQ c) $ ATransmission corrId (Conn cId) $ SENT (unId internalId) @@ -292,15 +335,21 @@ sendMessage c st corrId (Conn cId) msgBody = processBroadcastCommand :: forall c m. (AgentMonad m, EntityCommand 'Broadcast_ c) => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Broadcast_ -> ACommand 'Client c -> m () processBroadcastCommand c st corrId bcast@(Broadcast bId) = \case - NEW -> withStore (createBcast st bId) >> ok + NEW -> createNewBroadcast ADD (Conn cId) -> withStore (addBcastConn st bId cId) >> ok REM (Conn cId) -> withStore (removeBcastConn st bId cId) >> ok LS -> withStore (getBcast st bId) >>= respond bcast . MS . map Conn SEND msgBody -> withStore (getBcast st bId) >>= mapM_ (sendMsg msgBody) >> respond bcast (SENT 0) DEL -> withStore (deleteBcast st bId) >> ok where - sendMsg :: MsgBody -> ConnAlias -> m () - sendMsg msgBody cId = sendMessage c st corrId (Conn cId) msgBody + createNewBroadcast :: m () + createNewBroadcast = do + g <- asks idsDrg + bId' <- withStore $ createBcast st g bId + respond (Broadcast bId') OK + + sendMsg :: MsgBody -> ConnId -> m () + sendMsg msgBody cId = sendClientMessage c st corrId (Conn cId) msgBody ok :: m () ok = respond bcast OK @@ -308,7 +357,7 @@ processBroadcastCommand c st corrId bcast@(Broadcast bId) = \case respond :: EntityCommand t c' => Entity t -> ACommand 'Agent c' -> m () respond ent resp = atomically . writeTBQueue (sndQ c) $ ATransmission corrId ent resp -subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () +subscriber :: (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () subscriber c@AgentClient {msgQ} st = forever $ do -- TODO this will only process messages and notifications t <- atomically $ readTBQueue msgQ @@ -319,29 +368,33 @@ subscriber c@AgentClient {msgQ} st = forever $ do processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> SMPServerTransmission -> m () processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do withStore (getRcvConn st srv rId) >>= \case - SomeConn SCDuplex (DuplexConnection _ rq _) -> processSMP SCDuplex rq - SomeConn SCRcv (RcvConnection _ rq) -> processSMP SCRcv rq + SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq + SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq _ -> atomically . writeTBQueue sndQ $ ATransmission "" (Conn "") (ERR $ CONN SIMPLEX) where - processSMP :: SConnType c -> RcvQueue -> m () - processSMP cType rq@RcvQueue {connAlias, status} = + processSMP :: SConnType c -> ConnData -> RcvQueue -> m () + processSMP cType ConnData {connId} rq@RcvQueue {status} = case cmd of SMP.MSG srvMsgId srvTs msgBody -> do -- TODO deduplicate with previously received msg <- decryptAndVerify rq msgBody let msgHash = C.sha256Hash msg - agentMsg <- liftEither $ parseSMPMessage msg - case agentMsg of - SMPConfirmation senderKey -> smpConfirmation senderKey - SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> + case parseSMPMessage msg of + Left e -> notify $ ERR e + Right (SMPConfirmation senderKey) -> smpConfirmation senderKey + Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> case agentMessage of HELLO verifyKey _ -> helloMsg verifyKey msgBody REPLY qInfo -> replyMsg qInfo A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash + A_INTRO entity eInfo -> introMsg entity eInfo + A_INV conn qInfo cInfo -> invMsg conn qInfo cInfo + A_REQ conn qInfo cInfo -> reqMsg conn qInfo cInfo + A_CON conn -> conMsg conn sendAck c rq return () SMP.END -> do - removeSubscription c connAlias + removeSubscription c connId logServer "<--" c srv rId "END" notify END _ -> do @@ -349,7 +402,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do notify . ERR $ BROKER UNEXPECTED where notify :: EntityCommand 'Conn_ c => ACommand 'Agent c -> m () - notify msg = atomically . writeTBQueue sndQ $ ATransmission "" (Conn connAlias) msg + notify msg = atomically . writeTBQueue sndQ $ ATransmission "" (Conn connId) msg prohibited :: m () prohibited = notify . ERR $ AGENT A_PROHIBITED @@ -376,7 +429,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do void $ verifyMessage (Just verifyKey) msgBody withStore $ setRcvQueueActive st rq verifyKey case cType of - SCDuplex -> notify CON + SCDuplex -> connected _ -> pure () replyMsg :: SMPQueueInfo -> m () @@ -384,22 +437,87 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do logServer "<--" c srv rId "MSG " case cType of SCRcv -> do - (sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias - withStore $ upgradeRcvConnToDuplex st connAlias sq + (sq, senderKey, verifyKey) <- newSendQueue qInfo + withStore $ upgradeRcvConnToDuplex st connId sq connectToSendQueue c st sq senderKey verifyKey - notify CON + connected _ -> prohibited + connected :: m () + connected = do + withStore (getConnInvitation st connId) >>= \case + Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do + withStore $ setInvitationStatus st invId InvCon + sendControlMessage c sq $ A_CON (Conn externalIntroId) + _ -> pure () + notify CON + + introMsg :: IntroEntity -> EntityInfo -> m () + introMsg (IE entity) entityInfo = do + logServer "<--" c srv rId "MSG " + case (cType, entity) of + (SCDuplex, intro@Conn {}) -> createInv intro Nothing entityInfo + _ -> prohibited + + invMsg :: Entity 'Conn_ -> SMPQueueInfo -> EntityInfo -> m () + invMsg (Conn introId) qInfo toInfo = do + logServer "<--" c srv rId "MSG " + case cType of + SCDuplex -> + withStore (getIntro st introId) >>= \case + Introduction {toConn, toStatus = IntroNew, reConn, reStatus = IntroNew} + | toConn /= connId -> prohibited + | otherwise -> + withStore (addIntroInvitation st introId toInfo qInfo >> getConn st reConn) >>= \case + SomeConn _ (DuplexConnection _ _ sq) -> do + sendControlMessage c sq $ A_REQ (Conn introId) qInfo toInfo + withStore $ setIntroReStatus st introId IntroInv + _ -> prohibited + _ -> prohibited + _ -> prohibited + + reqMsg :: Entity 'Conn_ -> SMPQueueInfo -> EntityInfo -> m () + reqMsg intro qInfo connInfo = do + logServer "<--" c srv rId "MSG " + case cType of + SCDuplex -> createInv intro (Just qInfo) connInfo + _ -> prohibited + + createInv :: Entity 'Conn_ -> Maybe SMPQueueInfo -> EntityInfo -> m () + createInv (Conn externalIntroId) qInfo entityInfo = do + g <- asks idsDrg + let newInv = NewInvitation {viaConn = connId, externalIntroId, entityInfo, qInfo} + invId <- withStore $ createInvitation st g newInv + notify $ REQ (IE (Conn invId)) entityInfo + + conMsg :: Entity 'Conn_ -> m () + conMsg (Conn introId) = do + logServer "<--" c srv rId "MSG " + withStore (getIntro st introId) >>= \case + Introduction {toConn, toStatus, reConn, reStatus} + | toConn == connId && toStatus == IntroInv -> do + withStore $ setIntroToStatus st introId IntroCon + sendConMsg toConn reConn reStatus + | reConn == connId && reStatus == IntroInv -> do + withStore $ setIntroReStatus st introId IntroCon + sendConMsg toConn reConn toStatus + | otherwise -> prohibited + where + sendConMsg :: ConnId -> ConnId -> IntroStatus -> m () + sendConMsg toConn reConn IntroCon = + atomically . writeTBQueue sndQ $ ATransmission "" (Conn toConn) $ ICON $ IE (Conn reConn) + sendConMsg _ _ _ = pure () + agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () agentClientMsg receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do logServer "<--" c srv rId "MSG " case status of Active -> do internalTs <- liftIO getCurrentTime - (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st rq + (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st connId let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash receivedPrevMsgHash withStore $ - createRcvMsg st rq $ + createRcvMsg st connId $ RcvMsgData { internalId, internalRcvId, @@ -438,8 +556,8 @@ connectToSendQueue c st sq senderKey verifyKey = do withStore $ setSndQueueStatus st sq Active newSendQueue :: - (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> ConnAlias -> m (SndQueue, SenderPublicKey, VerificationKey) -newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do + (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) +newSendQueue (SMPQueueInfo smpServer senderId encryptKey) = do size <- asks $ rsaKeySize . config (senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size (verifyKey, signKey) <- liftIO $ C.generateKeyPair size @@ -447,7 +565,6 @@ newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do SndQueue { server = smpServer, sndId = senderId, - connAlias, sndPrivateKey, encryptKey, signKey, diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 2c6bea6f1..f440a6df9 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -15,6 +15,7 @@ module Simplex.Messaging.Agent.Client closeSMPServerClients, newReceiveQueue, subscribeQueue, + addSubscription, sendConfirmation, sendHello, secureQueue, @@ -61,8 +62,8 @@ data AgentClient = AgentClient sndQ :: TBQueue (ATransmission 'Agent), msgQ :: TBQueue SMPServerTransmission, smpClients :: TVar (Map SMPServer SMPClient), - subscrSrvrs :: TVar (Map SMPServer (Set ConnAlias)), - subscrConns :: TVar (Map ConnAlias SMPServer), + subscrSrvrs :: TVar (Map SMPServer (Set ConnId)), + subscrConns :: TVar (Map ConnId SMPServer), clientId :: Int } @@ -78,7 +79,7 @@ newAgentClient cc AgentConfig {tbqSize} = do writeTVar cc clientId return AgentClient {rcvQ, sndQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId} -type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) +type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m, MonadFail m) getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient getSMPServerClient c@AgentClient {smpClients, msgQ} srv = @@ -106,7 +107,7 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = removeSubs >>= mapM_ (mapM_ notifySub) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeSubs :: IO (Maybe (Set ConnAlias)) + removeSubs :: IO (Maybe (Set ConnId)) removeSubs = atomically $ do modifyTVar smpClients $ M.delete srv cs <- M.lookup srv <$> readTVar (subscrSrvrs c) @@ -117,7 +118,7 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = deleteKeys :: Ord k => Set k -> Map k a -> Map k a deleteKeys ks m = S.foldr' M.delete m ks - notifySub :: ConnAlias -> IO () + notifySub :: ConnId -> IO () notifySub connAlias = atomically . writeTBQueue (sndQ c) $ ATransmission "" (Conn connAlias) END closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m () @@ -158,8 +159,8 @@ smpClientError = \case SMPTransportError e -> BROKER $ TRANSPORT e e -> INTERNAL $ show e -newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> ConnAlias -> m (RcvQueue, SMPQueueInfo) -newReceiveQueue c srv connAlias = do +newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueInfo) +newReceiveQueue c srv = do size <- asks $ rsaKeySize . config (recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size logServer "-->" c srv "" "NEW" @@ -170,7 +171,6 @@ newReceiveQueue c srv connAlias = do RcvQueue { server = srv, rcvId, - connAlias, rcvPrivateKey, sndId = Just sId, sndKey = Nothing, @@ -178,25 +178,24 @@ newReceiveQueue c srv connAlias = do verifyKey = Nothing, status = New } - addSubscription c rq connAlias return (rq, SMPQueueInfo srv sId encryptKey) -subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnAlias -> m () +subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m () subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do withLogSMP c server rcvId "SUB" $ \smp -> subscribeSMPQueue smp rcvPrivateKey rcvId addSubscription c rq connAlias -addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnAlias -> m () +addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m () addSubscription c RcvQueue {server} connAlias = atomically $ do modifyTVar (subscrConns c) $ M.insert connAlias server modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server where - addSub :: Maybe (Set ConnAlias) -> Set ConnAlias + addSub :: Maybe (Set ConnId) -> Set ConnId addSub (Just cs) = S.insert connAlias cs addSub _ = S.singleton connAlias -removeSubscription :: AgentMonad m => AgentClient -> ConnAlias -> m () +removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m () removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do cs <- readTVar subscrConns writeTVar subscrConns $ M.delete connAlias cs @@ -204,7 +203,7 @@ removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically (modifyTVar subscrSrvrs . M.alter (>>= delSub)) (M.lookup connAlias cs) where - delSub :: Set ConnAlias -> Maybe (Set ConnAlias) + delSub :: Set ConnId -> Maybe (Set ConnId) delSub cs = let cs' = S.delete connAlias cs in if S.null cs' then Nothing else Just cs' diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 07f135440..16f579504 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -33,6 +33,8 @@ module Simplex.Messaging.Agent.Protocol Entity (..), EntityTag (..), AnEntity (..), + IntroEntity (..), + EntityInfo, EntityCommand, entityCommand, ACommand (..), @@ -53,7 +55,7 @@ module Simplex.Messaging.Agent.Protocol ATransmission (..), ATransmissionOrError (..), ARawTransmission, - ConnAlias, + ConnId, ReplyMode (..), AckMode (..), OnOff (..), @@ -171,6 +173,13 @@ deriving instance Eq (Entity t) deriving instance Show (Entity t) +instance TestEquality Entity where + testEquality (Conn c) (Conn c') = refl c c' + testEquality (OpenConn c) (OpenConn c') = refl c c' + testEquality (Broadcast c) (Broadcast c') = refl c c' + testEquality (AGroup c) (AGroup c') = refl c c' + testEquality _ _ = Nothing + entityId :: Entity t -> ByteString entityId = \case Conn bs -> bs @@ -195,7 +204,11 @@ type family EntityCommand (t :: EntityTag) (c :: ACmdTag) :: Constraint where EntityCommand Conn_ NEW_ = () EntityCommand Conn_ INV_ = () EntityCommand Conn_ JOIN_ = () + EntityCommand Conn_ INTRO_ = () + EntityCommand Conn_ REQ_ = () + EntityCommand Conn_ ACPT_ = () EntityCommand Conn_ CON_ = () + EntityCommand Conn_ ICON_ = () EntityCommand Conn_ SUB_ = () EntityCommand Conn_ SUBALL_ = () EntityCommand Conn_ END_ = () @@ -226,7 +239,11 @@ entityCommand = \case NEW -> Just Dict INV _ -> Just Dict JOIN {} -> Just Dict + INTRO {} -> Just Dict + REQ {} -> Just Dict + ACPT {} -> Just Dict CON -> Just Dict + ICON {} -> Just Dict SUB -> Just Dict SUBALL -> Just Dict END -> Just Dict @@ -258,7 +275,11 @@ data ACmdTag = NEW_ | INV_ | JOIN_ + | INTRO_ + | REQ_ + | ACPT_ | CON_ + | ICON_ | SUB_ | SUBALL_ | END_ @@ -274,15 +295,31 @@ data ACmdTag | OK_ | ERR_ +type family Introduction (t :: EntityTag) :: Constraint where + Introduction Conn_ = () + Introduction OpenConn_ = () + Introduction AGroup_ = () + Introduction t = (Int ~ Bool, TypeError (Text "Entity " :<>: ShowType t :<>: Text " cannot be INTRO'd to")) + +data IntroEntity = forall t. Introduction t => IE (Entity t) + +instance Eq IntroEntity where + IE e1 == IE e2 = isJust $ testEquality e1 e2 + +deriving instance Show IntroEntity + +type EntityInfo = ByteString + -- | Parameterized type for SMP agent protocol commands and responses from all participants. data ACommand (p :: AParty) (c :: ACmdTag) where NEW :: ACommand Client NEW_ -- response INV INV :: SMPQueueInfo -> ACommand Agent INV_ JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client JOIN_ -- response OK + INTRO :: IntroEntity -> EntityInfo -> ACommand Client INTRO_ + REQ :: IntroEntity -> EntityInfo -> ACommand Agent INTRO_ + ACPT :: IntroEntity -> EntityInfo -> ACommand Client ACPT_ CON :: ACommand Agent CON_ -- notification that connection is established - -- TODO currently it automatically allows whoever sends the confirmation - -- CONF :: OtherPartyId -> ACommand Agent - -- LET :: OtherPartyId -> ACommand Client + ICON :: IntroEntity -> ACommand Agent ICON_ SUB :: ACommand Client SUB_ SUBALL :: ACommand Client SUBALL_ -- TODO should be moved to chat protocol - hack for subscribing to all END :: ACommand Agent END_ @@ -318,6 +355,7 @@ instance TestEquality (ACommand p) where testEquality c@INV {} c'@INV {} = refl c c' testEquality c@JOIN {} c'@JOIN {} = refl c c' testEquality CON CON = Just Refl + testEquality c@ICON {} c'@ICON {} = refl c c' testEquality SUB SUB = Just Refl testEquality SUBALL SUBALL = Just Refl testEquality END END = Just Refl @@ -334,7 +372,7 @@ instance TestEquality (ACommand p) where testEquality c@ERR {} c'@ERR {} = refl c c' testEquality _ _ = Nothing -refl :: Eq (f a) => f a -> f a -> Maybe (a :~: a) +refl :: Eq a => a -> a -> Maybe (t :~: t) refl x x' = if x == x' then Just Refl else Nothing -- | SMP message formats. @@ -366,6 +404,14 @@ data AMessage where REPLY :: SMPQueueInfo -> AMessage -- | agent envelope for the client message A_MSG :: MsgBody -> AMessage + -- | agent message for introduction + A_INTRO :: IntroEntity -> EntityInfo -> AMessage + -- | agent envelope for the sent invitation + A_INV :: Entity Conn_ -> SMPQueueInfo -> EntityInfo -> AMessage + -- | agent envelope for the forwarded invitation + A_REQ :: Entity Conn_ -> SMPQueueInfo -> EntityInfo -> AMessage + -- | agent message for intro/group request + A_CON :: Entity Conn_ -> AMessage deriving (Show) -- | Parse SMP message. @@ -408,12 +454,22 @@ agentMessageP = "HELLO " *> hello <|> "REPLY " *> reply <|> "MSG " *> a_msg + <|> "INTRO " *> a_intro + <|> "INV " *> a_inv + <|> "REQ " *> a_req + <|> "CON " *> a_con where hello = HELLO <$> C.pubKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP - a_msg = do + a_msg = A_MSG <$> binaryBody + a_intro = A_INTRO <$> introEntityP <* A.space <*> binaryBody + a_inv = invP A_INV + a_req = invP A_REQ + a_con = A_CON <$> connEntityP + invP f = f <$> connEntityP <* A.space <*> smpQueueInfoP <* A.space <*> binaryBody + binaryBody = do size :: Int <- A.decimal <* A.endOfLine - A_MSG <$> A.take size <* A.endOfLine + A.take size <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) -- | SMP queue information parser. @@ -434,6 +490,13 @@ serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo A_MSG body -> "MSG " <> serializeMsg body <> "\n" + A_INTRO (IE entity) eInfo -> "INTRO " <> serializeIntro entity eInfo <> "\n" + A_INV conn qInfo eInfo -> "INV " <> serializeInv conn qInfo eInfo + A_REQ conn qInfo eInfo -> "REQ " <> serializeInv conn qInfo eInfo + A_CON conn -> "CON " <> serializeEntity conn + where + serializeInv conn qInfo eInfo = + B.intercalate " " [serializeEntity conn, serializeSmpQueueInfo qInfo, serializeMsg eInfo] <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString @@ -457,7 +520,7 @@ instance IsString SMPServer where fromString = parseString . parseAll $ smpServerP -- | SMP agent connection alias. -type ConnAlias = ByteString +type ConnId = ByteString -- | Connection modes. data OnOff = On | Off deriving (Eq, Show, Read) @@ -614,10 +677,19 @@ anEntityP = <|> "B:" $> AE . Broadcast <|> "G:" $> AE . AGroup ) - <*> A.takeTill (== ' ') + <*> A.takeTill wordEnd -entityConnP :: Parser (Entity Conn_) -entityConnP = "C:" *> (Conn <$> A.takeTill (== ' ')) +connEntityP :: Parser (Entity Conn_) +connEntityP = "C:" *> (Conn <$> A.takeTill wordEnd) + +introEntityP :: Parser IntroEntity +introEntityP = + ($) + <$> ( "C:" $> IE . Conn + <|> "O:" $> IE . OpenConn + <|> "G:" $> IE . AGroup + ) + <*> A.takeTill wordEnd serializeEntity :: Entity t -> ByteString serializeEntity = \case @@ -632,6 +704,9 @@ commandP = "NEW" $> ACmd SClient NEW <|> "INV " *> invResp <|> "JOIN " *> joinCmd + <|> "INTRO " *> introCmd + <|> "REQ " *> reqCmd + <|> "ACPT " *> acptCmd <|> "SUB" $> ACmd SClient SUB <|> "SUBALL" $> ACmd SClient SUBALL -- TODO remove - hack for subscribing to all <|> "END" $> ACmd SAgent END @@ -645,16 +720,21 @@ commandP = <|> "LS" $> ACmd SClient LS <|> "MS " *> membersResp <|> "ERR " *> agentError + <|> "ICON " *> iconMsg <|> "CON" $> ACmd SAgent CON <|> "OK" $> ACmd SAgent OK where invResp = ACmd SAgent . INV <$> smpQueueInfoP joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <*> replyMode) + introCmd = ACmd SClient <$> introP INTRO + reqCmd = ACmd SAgent <$> introP REQ + acptCmd = ACmd SClient <$> introP ACPT sendCmd = ACmd SClient . SEND <$> A.takeByteString sentResp = ACmd SAgent . SENT <$> A.decimal - addCmd = ACmd SClient . ADD <$> entityConnP - removeCmd = ACmd SClient . REM <$> entityConnP - membersResp = ACmd SAgent . MS <$> (entityConnP `A.sepBy'` A.char ' ') + addCmd = ACmd SClient . ADD <$> connEntityP + removeCmd = ACmd SClient . REM <$> connEntityP + membersResp = ACmd SAgent . MS <$> (connEntityP `A.sepBy'` A.char ' ') + iconMsg = ACmd SAgent . ICON <$> introEntityP message = do msgIntegrity <- msgIntegrityP <* A.space recipientMeta <- "R=" *> partyMeta A.decimal @@ -662,6 +742,7 @@ commandP = senderMeta <- "S=" *> partyMeta A.decimal msgBody <- A.takeByteString return $ ACmd SAgent MSG {recipientMeta, brokerMeta, senderMeta, msgIntegrity, msgBody} + introP f = f <$> introEntityP <* A.space <*> A.takeByteString replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On) partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space agentError = ACmd SAgent . ERR <$> agentErrorTypeP @@ -685,6 +766,9 @@ serializeCommand = \case NEW -> "NEW" INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode + INTRO (IE entity) eInfo -> "INTRO " <> serializeIntro entity eInfo + REQ (IE entity) eInfo -> "REQ " <> serializeIntro entity eInfo + ACPT (IE entity) eInfo -> "ACPT " <> serializeIntro entity eInfo SUB -> "SUB" SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all END -> "END" @@ -706,6 +790,7 @@ serializeCommand = \case LS -> "LS" MS cs -> "MS " <> B.intercalate " " (map serializeEntity cs) CON -> "CON" + ICON (IE entity) -> "ICON " <> serializeEntity entity ERR e -> "ERR " <> serializeAgentError e OK -> "OK" where @@ -716,6 +801,9 @@ serializeCommand = \case showTs :: UTCTime -> ByteString showTs = B.pack . formatISO8601Millis +serializeIntro :: Entity t -> ByteString -> ByteString +serializeIntro entity eInfo = serializeEntity entity <> " " <> serializeMsg eInfo + -- | Serialize message integrity validation result. serializeMsgIntegrity :: MsgIntegrity -> ByteString serializeMsgIntegrity = \case @@ -794,9 +882,10 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody hasEntityId :: AnEntity -> APartyCmd p -> Either AgentErrorType (APartyCmd p) hasEntityId (AE entity) (APartyCmd cmd) = APartyCmd <$> case cmd of - -- NEW and JOIN have optional entity + -- NEW, JOIN and ACPT have optional entity NEW -> Right cmd - JOIN _ _ -> Right cmd + JOIN {} -> Right cmd + ACPT {} -> Right cmd -- ERROR response does not always have entity ERR _ -> Right cmd -- other responses must have entity @@ -818,6 +907,9 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody APartyCmd <$$> case cmd of SEND body -> SEND <$$> getMsgBody body MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body + INTRO entity eInfo -> INTRO entity <$$> getMsgBody eInfo + REQ entity eInfo -> REQ entity <$$> getMsgBody eInfo + ACPT entity eInfo -> ACPT entity <$$> getMsgBody eInfo _ -> pure $ Right cmd -- TODO refactor with server diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 6d3dc606f..d1e71a3f6 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -3,16 +3,21 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} module Simplex.Messaging.Agent.Store where +import Control.Concurrent.STM (TVar) import Control.Exception (Exception) +import Crypto.Random (ChaChaDRG) import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Kind (Type) +import Data.Text (Text) import Data.Time (UTCTime) import Data.Type.Equality import Simplex.Messaging.Agent.Protocol @@ -30,33 +35,45 @@ import qualified Simplex.Messaging.Protocol as SMP -- | Store class type. Defines store access methods for implementations. class Monad m => MonadAgentStore s m where -- Queue and Connection management - createRcvConn :: s -> RcvQueue -> m () - createSndConn :: s -> SndQueue -> m () - getConn :: s -> ConnAlias -> m SomeConn - getAllConnAliases :: s -> m [ConnAlias] -- TODO remove - hack for subscribing to all + createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId + createSndConn :: s -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId + getConn :: s -> ConnId -> m SomeConn + getAllConnIds :: s -> m [ConnId] -- TODO remove - hack for subscribing to all getRcvConn :: s -> SMPServer -> SMP.RecipientId -> m SomeConn - deleteConn :: s -> ConnAlias -> m () - upgradeRcvConnToDuplex :: s -> ConnAlias -> SndQueue -> m () - upgradeSndConnToDuplex :: s -> ConnAlias -> RcvQueue -> m () + deleteConn :: s -> ConnId -> m () + upgradeRcvConnToDuplex :: s -> ConnId -> SndQueue -> m () + upgradeSndConnToDuplex :: s -> ConnId -> RcvQueue -> m () setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m () setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m () setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m () -- Msg management - updateRcvIds :: s -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) - createRcvMsg :: s -> RcvQueue -> RcvMsgData -> m () + updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) + createRcvMsg :: s -> ConnId -> RcvMsgData -> m () - updateSndIds :: s -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash) - createSndMsg :: s -> SndQueue -> SndMsgData -> m () + updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) + createSndMsg :: s -> ConnId -> SndMsgData -> m () - getMsg :: s -> ConnAlias -> InternalId -> m Msg + getMsg :: s -> ConnId -> InternalId -> m Msg -- Broadcasts - createBcast :: s -> BroadcastId -> m () - addBcastConn :: s -> BroadcastId -> ConnAlias -> m () - removeBcastConn :: s -> BroadcastId -> ConnAlias -> m () + createBcast :: s -> TVar ChaChaDRG -> BroadcastId -> m BroadcastId + addBcastConn :: s -> BroadcastId -> ConnId -> m () + removeBcastConn :: s -> BroadcastId -> ConnId -> m () deleteBcast :: s -> BroadcastId -> m () - getBcast :: s -> BroadcastId -> m [ConnAlias] + getBcast :: s -> BroadcastId -> m [ConnId] + + -- Introductions + createIntro :: s -> TVar ChaChaDRG -> NewIntroduction -> m IntroId + getIntro :: s -> IntroId -> m Introduction + addIntroInvitation :: s -> IntroId -> EntityInfo -> SMPQueueInfo -> m () + setIntroToStatus :: s -> IntroId -> IntroStatus -> m () + setIntroReStatus :: s -> IntroId -> IntroStatus -> m () + createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId + getInvitation :: s -> InvitationId -> m Invitation + addInvitationConn :: s -> InvitationId -> ConnId -> m () + getConnInvitation :: s -> ConnId -> m (Maybe (Invitation, Connection CDuplex)) + setInvitationStatus :: s -> InvitationId -> InvitationStatus -> m () -- * Queue types @@ -64,7 +81,6 @@ class Monad m => MonadAgentStore s m where data RcvQueue = RcvQueue { server :: SMPServer, rcvId :: SMP.RecipientId, - connAlias :: ConnAlias, rcvPrivateKey :: RecipientPrivateKey, sndId :: Maybe SMP.SenderId, sndKey :: Maybe SenderPublicKey, @@ -78,7 +94,6 @@ data RcvQueue = RcvQueue data SndQueue = SndQueue { server :: SMPServer, sndId :: SMP.SenderId, - connAlias :: ConnAlias, sndPrivateKey :: SenderPrivateKey, encryptKey :: EncryptionKey, signKey :: SignatureKey, @@ -102,9 +117,9 @@ data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show) -- - DuplexConnection is a connection that has both receive and send queues set up, -- typically created by upgrading a receive or a send connection with a missing queue. data Connection (d :: ConnType) where - RcvConnection :: ConnAlias -> RcvQueue -> Connection CRcv - SndConnection :: ConnAlias -> SndQueue -> Connection CSnd - DuplexConnection :: ConnAlias -> RcvQueue -> SndQueue -> Connection CDuplex + RcvConnection :: ConnData -> RcvQueue -> Connection CRcv + SndConnection :: ConnData -> SndQueue -> Connection CSnd + DuplexConnection :: ConnData -> RcvQueue -> SndQueue -> Connection CDuplex deriving instance Eq (Connection d) @@ -141,6 +156,9 @@ instance Eq SomeConn where deriving instance Show SomeConn +data ConnData = ConnData {connId :: ConnId, viaInv :: Maybe InvitationId, connLevel :: Int} + deriving (Eq, Show) + -- * Message integrity validation types type MsgHash = ByteString @@ -263,7 +281,7 @@ type DeliveredTs = UTCTime -- | Base message data independent of direction. data MsgBase = MsgBase - { connAlias :: ConnAlias, + { connAlias :: ConnId, -- | Monotonically increasing id of a message per connection, internal to the agent. -- Internal Id preserves ordering between both received and sent messages, and is needed -- to track the order of the conversation (which can be different for the sender / receiver) @@ -281,12 +299,87 @@ newtype InternalId = InternalId {unId :: Int64} deriving (Eq, Show) type InternalTs = UTCTime +-- * Introduction types + +data NewIntroduction = NewIntroduction + { toConn :: ConnId, + reConn :: ConnId, + reInfo :: ByteString + } + +data Introduction = Introduction + { introId :: IntroId, + toConn :: ConnId, + toInfo :: Maybe ByteString, + toStatus :: IntroStatus, + reConn :: ConnId, + reInfo :: ByteString, + reStatus :: IntroStatus, + qInfo :: Maybe SMPQueueInfo + } + +data IntroStatus = IntroNew | IntroInv | IntroCon + deriving (Eq) + +serializeIntroStatus :: IntroStatus -> Text +serializeIntroStatus = \case + IntroNew -> "" + IntroInv -> "INV" + IntroCon -> "CON" + +introStatusT :: Text -> Maybe IntroStatus +introStatusT = \case + "" -> Just IntroNew + "INV" -> Just IntroInv + "CON" -> Just IntroCon + _ -> Nothing + +type IntroId = ByteString + +data NewInvitation = NewInvitation + { viaConn :: ConnId, + externalIntroId :: IntroId, + entityInfo :: EntityInfo, + qInfo :: Maybe SMPQueueInfo + } + +data Invitation = Invitation + { invId :: InvitationId, + viaConn :: ConnId, + externalIntroId :: IntroId, + entityInfo :: EntityInfo, + qInfo :: Maybe SMPQueueInfo, + connId :: Maybe ConnId, + status :: InvitationStatus + } + deriving (Show) + +data InvitationStatus = InvNew | InvAcpt | InvCon + deriving (Eq, Show) + +serializeInvStatus :: InvitationStatus -> Text +serializeInvStatus = \case + InvNew -> "" + InvAcpt -> "ACPT" + InvCon -> "CON" + +invStatusT :: Text -> Maybe InvitationStatus +invStatusT = \case + "" -> Just InvNew + "ACPT" -> Just InvAcpt + "CON" -> Just InvCon + _ -> Nothing + +type InvitationId = ByteString + -- * Store errors -- | Agent store error. data StoreError = -- | IO exceptions in store actions. SEInternal ByteString + | -- | failed to generate unique random ID + SEUniqueID | -- | Connection alias not found (or both queues absent). SEConnNotFound | -- | Connection alias already used. @@ -298,6 +391,10 @@ data StoreError SEBcastNotFound | -- | Broadcast ID already used. SEBcastDuplicate + | -- | Introduction ID not found. + SEIntroNotFound + | -- | Invitation ID not found. + SEInvitationNotFound | -- | Currently not used. The intention was to pass current expected queue status in methods, -- as we always know what it should be at any stage of the protocol, -- and in case it does not match use this error. diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 9dcf7edd1..d619026c4 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -11,6 +12,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} @@ -22,14 +24,19 @@ module Simplex.Messaging.Agent.Store.SQLite where import Control.Concurrent (threadDelay) -import Control.Monad (unless, when) +import Control.Concurrent.STM (TVar, atomically, stateTVar) +import Control.Monad (join, unless, when) import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) import Control.Monad.IO.Unlift (MonadUnliftIO) +import Crypto.Random (ChaChaDRG, randomBytesGenerate) import Data.Bifunctor (first) +import Data.ByteString (ByteString) +import Data.ByteString.Base64 (encode) import Data.Char (toLower) +import Data.Functor (($>)) import Data.List (find) import Data.Maybe (fromMaybe) -import Data.Text (isPrefixOf) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8) import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), SQLData (..), SQLError, field) @@ -66,8 +73,8 @@ createSQLiteStore dbFilePath = do let dbDir = takeDirectory dbFilePath createDirectoryIfMissing False dbDir store <- connectSQLiteStore dbFilePath - compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]] - let threadsafeOption = find (isPrefixOf "THREADSAFE=") (concat compileOptions) + compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[Text]] + let threadsafeOption = find (T.isPrefixOf "THREADSAFE=") (concat compileOptions) case threadsafeOption of Just "THREADSAFE=0" -> confirmOrExit "SQLite compiled with non-threadsafe code." Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found" @@ -107,13 +114,16 @@ connectSQLiteStore dbFilePath = do |] pure SQLiteStore {dbFilePath, dbConn, dbNew} -checkConstraint :: StoreError -> IO () -> IO (Either StoreError ()) -checkConstraint err action = first handleError <$> E.try action - where - handleError :: SQLError -> StoreError - handleError e - | DB.sqlError e == DB.ErrorConstraint = err - | otherwise = SEInternal $ bshow e +checkConstraint :: StoreError -> IO a -> IO (Either StoreError a) +checkConstraint err action = first (handleSQLError err) <$> E.try action + +checkConstraint' :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) +checkConstraint' err action = action `E.catch` (pure . Left . handleSQLError err) + +handleSQLError :: StoreError -> SQLError -> StoreError +handleSQLError err e + | DB.sqlError e == DB.ErrorConstraint = err + | otherwise = SEInternal $ bshow e withTransaction :: forall a. DB.Connection -> IO a -> IO a withTransaction db a = loop 100 100_000 @@ -128,33 +138,43 @@ withTransaction db a = loop 100 100_000 else E.throwIO e instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteStore m where - createRcvConn :: SQLiteStore -> RcvQueue -> m () - createRcvConn SQLiteStore {dbConn} q@RcvQueue {server} = - liftIOEither $ - checkConstraint SEConnDuplicate $ - withTransaction dbConn $ do - upsertServer_ dbConn server - insertRcvQueue_ dbConn q - insertRcvConnection_ dbConn q + createRcvConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId + createRcvConn SQLiteStore {dbConn} gVar cData q@RcvQueue {server} = + -- TODO if schema has to be restarted, this function can be refactored + -- to create connection first using createWithRandomId + liftIOEither . checkConstraint' SEConnDuplicate . withTransaction dbConn $ + getConnId_ dbConn gVar cData >>= traverse create + where + create :: ConnId -> IO ConnId + create connId = do + upsertServer_ dbConn server + insertRcvQueue_ dbConn connId q + insertRcvConnection_ dbConn cData {connId} q + pure connId - createSndConn :: SQLiteStore -> SndQueue -> m () - createSndConn SQLiteStore {dbConn} q@SndQueue {server} = - liftIOEither $ - checkConstraint SEConnDuplicate $ - withTransaction dbConn $ do - upsertServer_ dbConn server - insertSndQueue_ dbConn q - insertSndConnection_ dbConn q + createSndConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId + createSndConn SQLiteStore {dbConn} gVar cData q@SndQueue {server} = + -- TODO if schema has to be restarted, this function can be refactored + -- to create connection first using createWithRandomId + liftIOEither . checkConstraint' SEConnDuplicate . withTransaction dbConn $ + getConnId_ dbConn gVar cData >>= traverse create + where + create :: ConnId -> IO ConnId + create connId = do + upsertServer_ dbConn server + insertSndQueue_ dbConn connId q + insertSndConnection_ dbConn cData {connId} q + pure connId - getConn :: SQLiteStore -> ConnAlias -> m SomeConn - getConn SQLiteStore {dbConn} connAlias = + getConn :: SQLiteStore -> ConnId -> m SomeConn + getConn SQLiteStore {dbConn} connId = liftIOEither . withTransaction dbConn $ - getConn_ dbConn connAlias + getConn_ dbConn connId - getAllConnAliases :: SQLiteStore -> m [ConnAlias] - getAllConnAliases SQLiteStore {dbConn} = + getAllConnIds :: SQLiteStore -> m [ConnId] + getAllConnIds SQLiteStore {dbConn} = liftIO $ do - r <- DB.query_ dbConn "SELECT conn_alias FROM connections;" :: IO [[ConnAlias]] + r <- DB.query_ dbConn "SELECT conn_alias FROM connections;" :: IO [[ConnId]] return (concat r) getRcvConn :: SQLiteStore -> SMPServer -> SMP.RecipientId -> m SomeConn @@ -169,37 +189,37 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId] >>= \case - [Only connAlias] -> getConn_ dbConn connAlias + [Only connId] -> getConn_ dbConn connId _ -> pure $ Left SEConnNotFound - deleteConn :: SQLiteStore -> ConnAlias -> m () - deleteConn SQLiteStore {dbConn} connAlias = + deleteConn :: SQLiteStore -> ConnId -> m () + deleteConn SQLiteStore {dbConn} connId = liftIO $ DB.executeNamed dbConn "DELETE FROM connections WHERE conn_alias = :conn_alias;" - [":conn_alias" := connAlias] + [":conn_alias" := connId] - upgradeRcvConnToDuplex :: SQLiteStore -> ConnAlias -> SndQueue -> m () - upgradeRcvConnToDuplex SQLiteStore {dbConn} connAlias sq@SndQueue {server} = + upgradeRcvConnToDuplex :: SQLiteStore -> ConnId -> SndQueue -> m () + upgradeRcvConnToDuplex SQLiteStore {dbConn} connId sq@SndQueue {server} = liftIOEither . withTransaction dbConn $ - getConn_ dbConn connAlias >>= \case + getConn_ dbConn connId >>= \case Right (SomeConn _ RcvConnection {}) -> do upsertServer_ dbConn server - insertSndQueue_ dbConn sq - updateConnWithSndQueue_ dbConn connAlias sq + insertSndQueue_ dbConn connId sq + updateConnWithSndQueue_ dbConn connId sq pure $ Right () Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c _ -> pure $ Left SEConnNotFound - upgradeSndConnToDuplex :: SQLiteStore -> ConnAlias -> RcvQueue -> m () - upgradeSndConnToDuplex SQLiteStore {dbConn} connAlias rq@RcvQueue {server} = + upgradeSndConnToDuplex :: SQLiteStore -> ConnId -> RcvQueue -> m () + upgradeSndConnToDuplex SQLiteStore {dbConn} connId rq@RcvQueue {server} = liftIOEither . withTransaction dbConn $ - getConn_ dbConn connAlias >>= \case + getConn_ dbConn connId >>= \case Right (SomeConn _ SndConnection {}) -> do upsertServer_ dbConn server - insertRcvQueue_ dbConn rq - updateConnWithRcvQueue_ dbConn connAlias rq + insertRcvQueue_ dbConn connId rq + updateConnWithRcvQueue_ dbConn connId rq pure $ Right () Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c _ -> pure $ Left SEConnNotFound @@ -248,83 +268,233 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] - updateRcvIds :: SQLiteStore -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) - updateRcvIds SQLiteStore {dbConn} RcvQueue {connAlias} = + updateRcvIds :: SQLiteStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) + updateRcvIds SQLiteStore {dbConn} connId = liftIO . withTransaction dbConn $ do - (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ dbConn connAlias + (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ dbConn connId let internalId = InternalId $ unId lastInternalId + 1 internalRcvId = InternalRcvId $ unRcvId lastInternalRcvId + 1 - updateLastIdsRcv_ dbConn connAlias internalId internalRcvId + updateLastIdsRcv_ dbConn connId internalId internalRcvId pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash) - createRcvMsg :: SQLiteStore -> RcvQueue -> RcvMsgData -> m () - createRcvMsg SQLiteStore {dbConn} RcvQueue {connAlias} rcvMsgData = + createRcvMsg :: SQLiteStore -> ConnId -> RcvMsgData -> m () + createRcvMsg SQLiteStore {dbConn} connId rcvMsgData = liftIO . withTransaction dbConn $ do - insertRcvMsgBase_ dbConn connAlias rcvMsgData - insertRcvMsgDetails_ dbConn connAlias rcvMsgData - updateHashRcv_ dbConn connAlias rcvMsgData + insertRcvMsgBase_ dbConn connId rcvMsgData + insertRcvMsgDetails_ dbConn connId rcvMsgData + updateHashRcv_ dbConn connId rcvMsgData - updateSndIds :: SQLiteStore -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash) - updateSndIds SQLiteStore {dbConn} SndQueue {connAlias} = + updateSndIds :: SQLiteStore -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) + updateSndIds SQLiteStore {dbConn} connId = liftIO . withTransaction dbConn $ do - (lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ dbConn connAlias + (lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ dbConn connId let internalId = InternalId $ unId lastInternalId + 1 internalSndId = InternalSndId $ unSndId lastInternalSndId + 1 - updateLastIdsSnd_ dbConn connAlias internalId internalSndId + updateLastIdsSnd_ dbConn connId internalId internalSndId pure (internalId, internalSndId, prevSndHash) - createSndMsg :: SQLiteStore -> SndQueue -> SndMsgData -> m () - createSndMsg SQLiteStore {dbConn} SndQueue {connAlias} sndMsgData = + createSndMsg :: SQLiteStore -> ConnId -> SndMsgData -> m () + createSndMsg SQLiteStore {dbConn} connId sndMsgData = liftIO . withTransaction dbConn $ do - insertSndMsgBase_ dbConn connAlias sndMsgData - insertSndMsgDetails_ dbConn connAlias sndMsgData - updateHashSnd_ dbConn connAlias sndMsgData + insertSndMsgBase_ dbConn connId sndMsgData + insertSndMsgDetails_ dbConn connId sndMsgData + updateHashSnd_ dbConn connId sndMsgData - getMsg :: SQLiteStore -> ConnAlias -> InternalId -> m Msg + getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg getMsg _st _connAlias _id = throwError SENotImplemented - createBcast :: SQLiteStore -> BroadcastId -> m () - createBcast SQLiteStore {dbConn} bId = - liftIOEither $ - checkConstraint SEBcastDuplicate $ - DB.execute dbConn "INSERT INTO broadcasts (broadcast_id) VALUES (?);" (Only bId) + createBcast :: SQLiteStore -> TVar ChaChaDRG -> BroadcastId -> m BroadcastId + createBcast SQLiteStore {dbConn} gVar bcastId = liftIOEither $ case bcastId of + "" -> createWithRandomId gVar create + bId -> checkConstraint SEBcastDuplicate $ create bId $> bId + where + create bId = DB.execute dbConn "INSERT INTO broadcasts (broadcast_id) VALUES (?);" (Only bId) - addBcastConn :: SQLiteStore -> BroadcastId -> ConnAlias -> m () - addBcastConn SQLiteStore {dbConn} bId connAlias = + addBcastConn :: SQLiteStore -> BroadcastId -> ConnId -> m () + addBcastConn SQLiteStore {dbConn} bId connId = liftIOEither . checkBroadcast dbConn bId $ - getConn_ dbConn connAlias >>= \case + getConn_ dbConn connId >>= \case Left _ -> pure $ Left SEConnNotFound Right (SomeConn _ RcvConnection {}) -> pure . Left $ SEBadConnType CRcv Right _ -> checkConstraint SEConnDuplicate $ DB.execute dbConn - "INSERT INTO broadcast_connections (broadcast_id, conn_alias) VALUES (?, ?);" - (bId, connAlias) + [sql| + INSERT INTO broadcast_connections + (broadcast_id, conn_alias) VALUES (?, ?); + |] + (bId, connId) - removeBcastConn :: SQLiteStore -> BroadcastId -> ConnAlias -> m () - removeBcastConn SQLiteStore {dbConn} bId connAlias = + removeBcastConn :: SQLiteStore -> BroadcastId -> ConnId -> m () + removeBcastConn SQLiteStore {dbConn} bId connId = liftIOEither . checkBroadcast dbConn bId $ - bcastConnExists_ dbConn bId connAlias >>= \case + bcastConnExists_ dbConn bId connId >>= \case False -> pure $ Left SEConnNotFound _ -> Right <$> DB.execute dbConn - "DELETE FROM broadcast_connections WHERE broadcast_id = ? AND conn_alias = ?;" - (bId, connAlias) + [sql| + DELETE FROM broadcast_connections + WHERE broadcast_id = ? AND conn_alias = ?; + |] + (bId, connId) deleteBcast :: SQLiteStore -> BroadcastId -> m () deleteBcast SQLiteStore {dbConn} bId = liftIOEither . checkBroadcast dbConn bId $ Right <$> DB.execute dbConn "DELETE FROM broadcasts WHERE broadcast_id = ?;" (Only bId) - getBcast :: SQLiteStore -> BroadcastId -> m [ConnAlias] + getBcast :: SQLiteStore -> BroadcastId -> m [ConnId] getBcast SQLiteStore {dbConn} bId = liftIOEither . checkBroadcast dbConn bId $ Right . map fromOnly <$> DB.query dbConn "SELECT conn_alias FROM broadcast_connections WHERE broadcast_id = ?;" (Only bId) + createIntro :: SQLiteStore -> TVar ChaChaDRG -> NewIntroduction -> m IntroId + createIntro SQLiteStore {dbConn} gVar NewIntroduction {toConn, reConn, reInfo} = + liftIOEither . createWithRandomId gVar $ \introId -> + DB.execute + dbConn + [sql| + INSERT INTO conn_intros + (intro_id, to_conn, re_conn, re_info) VALUES (?, ?, ?, ?); + |] + (introId, toConn, reConn, reInfo) + + getIntro :: SQLiteStore -> IntroId -> m Introduction + getIntro SQLiteStore {dbConn} introId = + liftIOEither $ + intro + <$> DB.query + dbConn + [sql| + SELECT to_conn, to_info, to_status, re_conn, re_info, re_status, queue_info + FROM conn_intros + WHERE intro_id = ?; + |] + (Only introId) + where + intro [(toConn, toInfo, toStatus, reConn, reInfo, reStatus, qInfo)] = + Right $ Introduction {introId, toConn, toInfo, toStatus, reConn, reInfo, reStatus, qInfo} + intro _ = Left SEIntroNotFound + + addIntroInvitation :: SQLiteStore -> IntroId -> EntityInfo -> SMPQueueInfo -> m () + addIntroInvitation SQLiteStore {dbConn} introId toInfo qInfo = + liftIO $ + DB.executeNamed + dbConn + [sql| + UPDATE conn_intros + SET to_info = :to_info, + queue_info = :queue_info, + to_status = :to_status + WHERE intro_id = :intro_id; + |] + [ ":to_info" := toInfo, + ":queue_info" := Just qInfo, + ":to_status" := IntroInv, + ":intro_id" := introId + ] + + setIntroToStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () + setIntroToStatus SQLiteStore {dbConn} introId toStatus = + liftIO $ + DB.execute + dbConn + [sql| + UPDATE conn_intros + SET to_status = ? + WHERE intro_id = ?; + |] + (toStatus, introId) + + setIntroReStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () + setIntroReStatus SQLiteStore {dbConn} introId reStatus = + liftIO $ + DB.execute + dbConn + [sql| + UPDATE conn_intros + SET re_status = ? + WHERE intro_id = ?; + |] + (reStatus, introId) + + createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId + createInvitation SQLiteStore {dbConn} gVar NewInvitation {viaConn, externalIntroId, entityInfo, qInfo} = + liftIOEither . createWithRandomId gVar $ \invId -> + DB.execute + dbConn + [sql| + INSERT INTO conn_invitations + (inv_id, via_conn, external_intro_id, conn_info, queue_info) VALUES (?, ?, ?, ?, ?); + |] + (invId, viaConn, externalIntroId, entityInfo, qInfo) + + getInvitation :: SQLiteStore -> InvitationId -> m Invitation + getInvitation SQLiteStore {dbConn} invId = + liftIOEither $ + invitation + <$> DB.query + dbConn + [sql| + SELECT via_conn, external_intro_id, conn_info, queue_info, conn_id, status + FROM conn_invitations + WHERE inv_id = ?; + |] + (Only invId) + where + invitation [(viaConn, externalIntroId, entityInfo, qInfo, connId, status)] = + Right $ Invitation {invId, viaConn, externalIntroId, entityInfo, qInfo, connId, status} + invitation _ = Left SEInvitationNotFound + + addInvitationConn :: SQLiteStore -> InvitationId -> ConnId -> m () + addInvitationConn SQLiteStore {dbConn} invId connId = + liftIO $ + DB.executeNamed + dbConn + [sql| + UPDATE conn_invitations + SET conn_id = :conn_id, status = :status + WHERE inv_id = :inv_id; + |] + [":conn_id" := connId, ":status" := InvAcpt, ":inv_id" := invId] + + getConnInvitation :: SQLiteStore -> ConnId -> m (Maybe (Invitation, Connection 'CDuplex)) + getConnInvitation SQLiteStore {dbConn} cId = + liftIO . withTransaction dbConn $ + DB.query + dbConn + [sql| + SELECT inv_id, via_conn, external_intro_id, conn_info, queue_info, status + FROM conn_invitations + WHERE conn_id = ?; + |] + (Only cId) + >>= fmap join . traverse getViaConn . invitation + where + invitation [(invId, viaConn, externalIntroId, entityInfo, qInfo, status)] = + Just $ Invitation {invId, viaConn, externalIntroId, entityInfo, qInfo, connId = Just cId, status} + invitation _ = Nothing + getViaConn :: Invitation -> IO (Maybe (Invitation, Connection 'CDuplex)) + getViaConn inv@Invitation {viaConn} = fmap (inv,) . duplexConn <$> getConn_ dbConn viaConn + duplexConn :: Either StoreError SomeConn -> Maybe (Connection 'CDuplex) + duplexConn (Right (SomeConn SCDuplex conn)) = Just conn + duplexConn _ = Nothing + + setInvitationStatus :: SQLiteStore -> InvitationId -> InvitationStatus -> m () + setInvitationStatus SQLiteStore {dbConn} invId status = + liftIO $ + DB.execute + dbConn + [sql| + UPDATE conn_invitations + SET status = ? WHERE inv_id = ?; + |] + (status, invId) + -- * Auxiliary helpers -- ? replace with ToField? - it's easy to forget to use this @@ -337,7 +507,7 @@ deserializePort_ port = Just port instance ToField QueueStatus where toField = toField . show -instance FromField QueueStatus where fromField = fromFieldToReadable_ +instance FromField QueueStatus where fromField = fromTextField_ $ readMaybe . T.unpack instance ToField InternalRcvId where toField (InternalRcvId x) = toField x @@ -359,13 +529,24 @@ instance ToField MsgIntegrity where toField = toField . serializeMsgIntegrity instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP -fromFieldToReadable_ :: forall a. (Read a, E.Typeable a) => Field -> Ok a -fromFieldToReadable_ = \case +instance ToField IntroStatus where toField = toField . serializeIntroStatus + +instance FromField IntroStatus where fromField = fromTextField_ introStatusT + +instance ToField InvitationStatus where toField = toField . serializeInvStatus + +instance FromField InvitationStatus where fromField = fromTextField_ invStatusT + +instance ToField SMPQueueInfo where toField = toField . serializeSmpQueueInfo + +instance FromField SMPQueueInfo where fromField = blobFieldParser smpQueueInfoP + +fromTextField_ :: (E.Typeable a) => (Text -> Maybe a) -> Field -> Ok a +fromTextField_ fromText = \case f@(Field (SQLText t) _) -> - let str = T.unpack t - in case readMaybe str of - Just x -> Ok x - _ -> returnError ConversionFailed f ("invalid string: " <> str) + case fromText t of + Just x -> Ok x + _ -> returnError ConversionFailed f ("invalid text: " <> T.unpack t) f -> returnError ConversionFailed f "expecting SQLText column type" {- ORMOLU_DISABLE -} @@ -397,8 +578,8 @@ upsertServer_ dbConn SMPServer {host, port, keyHash} = do -- * createRcvConn helpers -insertRcvQueue_ :: DB.Connection -> RcvQueue -> IO () -insertRcvQueue_ dbConn RcvQueue {..} = do +insertRcvQueue_ :: DB.Connection -> ConnId -> RcvQueue -> IO () +insertRcvQueue_ dbConn connId RcvQueue {..} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn @@ -411,7 +592,7 @@ insertRcvQueue_ dbConn RcvQueue {..} = do [ ":host" := host server, ":port" := port_, ":rcv_id" := rcvId, - ":conn_alias" := connAlias, + ":conn_alias" := connId, ":rcv_private_key" := rcvPrivateKey, ":snd_id" := sndId, ":snd_key" := sndKey, @@ -420,26 +601,29 @@ insertRcvQueue_ dbConn RcvQueue {..} = do ":status" := status ] -insertRcvConnection_ :: DB.Connection -> RcvQueue -> IO () -insertRcvConnection_ dbConn RcvQueue {server, rcvId, connAlias} = do +insertRcvConnection_ :: DB.Connection -> ConnData -> RcvQueue -> IO () +insertRcvConnection_ dbConn ConnData {connId, viaInv, connLevel} RcvQueue {server, rcvId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn [sql| INSERT INTO connections - ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, - last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, - last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) + ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, via_inv, conn_level, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) VALUES - (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, - 0, 0, 0, 0, x'', x''); + (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, :via_inv,:conn_level, 0, 0, 0, 0, x'', x''); |] - [":conn_alias" := connAlias, ":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId] + [ ":conn_alias" := connId, + ":rcv_host" := host server, + ":rcv_port" := port_, + ":rcv_id" := rcvId, + ":via_inv" := viaInv, + ":conn_level" := connLevel + ] -- * createSndConn helpers -insertSndQueue_ :: DB.Connection -> SndQueue -> IO () -insertSndQueue_ dbConn SndQueue {..} = do +insertSndQueue_ :: DB.Connection -> ConnId -> SndQueue -> IO () +insertSndQueue_ dbConn connId SndQueue {..} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn @@ -452,85 +636,96 @@ insertSndQueue_ dbConn SndQueue {..} = do [ ":host" := host server, ":port" := port_, ":snd_id" := sndId, - ":conn_alias" := connAlias, + ":conn_alias" := connId, ":snd_private_key" := sndPrivateKey, ":encrypt_key" := encryptKey, ":sign_key" := signKey, ":status" := status ] -insertSndConnection_ :: DB.Connection -> SndQueue -> IO () -insertSndConnection_ dbConn SndQueue {server, sndId, connAlias} = do +insertSndConnection_ :: DB.Connection -> ConnData -> SndQueue -> IO () +insertSndConnection_ dbConn ConnData {connId, viaInv, connLevel} SndQueue {server, sndId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn [sql| INSERT INTO connections - ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, - last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, - last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) + ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, via_inv, conn_level, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) VALUES - (:conn_alias, NULL, NULL, NULL,:snd_host,:snd_port,:snd_id, - 0, 0, 0, 0, x'', x''); + (:conn_alias, NULL, NULL, NULL, :snd_host,:snd_port,:snd_id,:via_inv,:conn_level, 0, 0, 0, 0, x'', x''); |] - [":conn_alias" := connAlias, ":snd_host" := host server, ":snd_port" := port_, ":snd_id" := sndId] + [ ":conn_alias" := connId, + ":snd_host" := host server, + ":snd_port" := port_, + ":snd_id" := sndId, + ":via_inv" := viaInv, + ":conn_level" := connLevel + ] -- * getConn helpers -getConn_ :: DB.Connection -> ConnAlias -> IO (Either StoreError SomeConn) -getConn_ dbConn connAlias = do - rQ <- retrieveRcvQueueByConnAlias_ dbConn connAlias - sQ <- retrieveSndQueueByConnAlias_ dbConn connAlias - pure $ case (rQ, sQ) of - (Just rcvQ, Just sndQ) -> Right $ SomeConn SCDuplex (DuplexConnection connAlias rcvQ sndQ) - (Just rcvQ, Nothing) -> Right $ SomeConn SCRcv (RcvConnection connAlias rcvQ) - (Nothing, Just sndQ) -> Right $ SomeConn SCSnd (SndConnection connAlias sndQ) - _ -> Left SEConnNotFound +getConn_ :: DB.Connection -> ConnId -> IO (Either StoreError SomeConn) +getConn_ dbConn connId = + getConnData_ dbConn connId >>= \case + Nothing -> pure $ Left SEConnNotFound + Just connData -> do + rQ <- getRcvQueueByConnAlias_ dbConn connId + sQ <- getSndQueueByConnAlias_ dbConn connId + pure $ case (rQ, sQ) of + (Just rcvQ, Just sndQ) -> Right $ SomeConn SCDuplex (DuplexConnection connData rcvQ sndQ) + (Just rcvQ, Nothing) -> Right $ SomeConn SCRcv (RcvConnection connData rcvQ) + (Nothing, Just sndQ) -> Right $ SomeConn SCSnd (SndConnection connData sndQ) + _ -> Left SEConnNotFound -retrieveRcvQueueByConnAlias_ :: DB.Connection -> ConnAlias -> IO (Maybe RcvQueue) -retrieveRcvQueueByConnAlias_ dbConn connAlias = do - r <- - DB.queryNamed +getConnData_ :: DB.Connection -> ConnId -> IO (Maybe ConnData) +getConnData_ dbConn connId = + connData + <$> DB.query dbConn "SELECT via_inv, conn_level FROM connections WHERE conn_alias = ?;" (Only connId) + where + connData [(viaInv, connLevel)] = Just ConnData {connId, viaInv, connLevel} + connData _ = Nothing + +getRcvQueueByConnAlias_ :: DB.Connection -> ConnId -> IO (Maybe RcvQueue) +getRcvQueueByConnAlias_ dbConn connId = + rcvQueue + <$> DB.query dbConn [sql| - SELECT - s.key_hash, q.host, q.port, q.rcv_id, q.conn_alias, q.rcv_private_key, + SELECT s.key_hash, q.host, q.port, q.rcv_id, q.rcv_private_key, q.snd_id, q.snd_key, q.decrypt_key, q.verify_key, q.status FROM rcv_queues q INNER JOIN servers s ON q.host = s.host AND q.port = s.port - WHERE q.conn_alias = :conn_alias; + WHERE q.conn_alias = ?; |] - [":conn_alias" := connAlias] - case r of - [(keyHash, host, port, rcvId, cAlias, rcvPrivateKey, sndId, sndKey, decryptKey, verifyKey, status)] -> do + (Only connId) + where + rcvQueue [(keyHash, host, port, rcvId, rcvPrivateKey, sndId, sndKey, decryptKey, verifyKey, status)] = let srv = SMPServer host (deserializePort_ port) keyHash - return . Just $ RcvQueue srv rcvId cAlias rcvPrivateKey sndId sndKey decryptKey verifyKey status - _ -> return Nothing + in Just $ RcvQueue srv rcvId rcvPrivateKey sndId sndKey decryptKey verifyKey status + rcvQueue _ = Nothing -retrieveSndQueueByConnAlias_ :: DB.Connection -> ConnAlias -> IO (Maybe SndQueue) -retrieveSndQueueByConnAlias_ dbConn connAlias = do - r <- - DB.queryNamed +getSndQueueByConnAlias_ :: DB.Connection -> ConnId -> IO (Maybe SndQueue) +getSndQueueByConnAlias_ dbConn connId = + sndQueue + <$> DB.query dbConn [sql| - SELECT - s.key_hash, q.host, q.port, q.snd_id, q.conn_alias, - q.snd_private_key, q.encrypt_key, q.sign_key, q.status + SELECT s.key_hash, q.host, q.port, q.snd_id, q.snd_private_key, q.encrypt_key, q.sign_key, q.status FROM snd_queues q INNER JOIN servers s ON q.host = s.host AND q.port = s.port - WHERE q.conn_alias = :conn_alias; + WHERE q.conn_alias = ?; |] - [":conn_alias" := connAlias] - case r of - [(keyHash, host, port, sndId, cAlias, sndPrivateKey, encryptKey, signKey, status)] -> do + (Only connId) + where + sndQueue [(keyHash, host, port, sndId, sndPrivateKey, encryptKey, signKey, status)] = let srv = SMPServer host (deserializePort_ port) keyHash - return . Just $ SndQueue srv sndId cAlias sndPrivateKey encryptKey signKey status - _ -> return Nothing + in Just $ SndQueue srv sndId sndPrivateKey encryptKey signKey status + sndQueue _ = Nothing -- * upgradeRcvConnToDuplex helpers -updateConnWithSndQueue_ :: DB.Connection -> ConnAlias -> SndQueue -> IO () -updateConnWithSndQueue_ dbConn connAlias SndQueue {server, sndId} = do +updateConnWithSndQueue_ :: DB.Connection -> ConnId -> SndQueue -> IO () +updateConnWithSndQueue_ dbConn connId SndQueue {server, sndId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn @@ -539,12 +734,12 @@ updateConnWithSndQueue_ dbConn connAlias SndQueue {server, sndId} = do SET snd_host = :snd_host, snd_port = :snd_port, snd_id = :snd_id WHERE conn_alias = :conn_alias; |] - [":snd_host" := host server, ":snd_port" := port_, ":snd_id" := sndId, ":conn_alias" := connAlias] + [":snd_host" := host server, ":snd_port" := port_, ":snd_id" := sndId, ":conn_alias" := connId] -- * upgradeSndConnToDuplex helpers -updateConnWithRcvQueue_ :: DB.Connection -> ConnAlias -> RcvQueue -> IO () -updateConnWithRcvQueue_ dbConn connAlias RcvQueue {server, rcvId} = do +updateConnWithRcvQueue_ :: DB.Connection -> ConnId -> RcvQueue -> IO () +updateConnWithRcvQueue_ dbConn connId RcvQueue {server, rcvId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn @@ -553,12 +748,12 @@ updateConnWithRcvQueue_ dbConn connAlias RcvQueue {server, rcvId} = do SET rcv_host = :rcv_host, rcv_port = :rcv_port, rcv_id = :rcv_id WHERE conn_alias = :conn_alias; |] - [":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId, ":conn_alias" := connAlias] + [":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId, ":conn_alias" := connId] -- * updateRcvIds helpers -retrieveLastIdsAndHashRcv_ :: DB.Connection -> ConnAlias -> IO (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) -retrieveLastIdsAndHashRcv_ dbConn connAlias = do +retrieveLastIdsAndHashRcv_ :: DB.Connection -> ConnId -> IO (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) +retrieveLastIdsAndHashRcv_ dbConn connId = do [(lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash)] <- DB.queryNamed dbConn @@ -567,11 +762,11 @@ retrieveLastIdsAndHashRcv_ dbConn connAlias = do FROM connections WHERE conn_alias = :conn_alias; |] - [":conn_alias" := connAlias] + [":conn_alias" := connId] return (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) -updateLastIdsRcv_ :: DB.Connection -> ConnAlias -> InternalId -> InternalRcvId -> IO () -updateLastIdsRcv_ dbConn connAlias newInternalId newInternalRcvId = +updateLastIdsRcv_ :: DB.Connection -> ConnId -> InternalId -> InternalRcvId -> IO () +updateLastIdsRcv_ dbConn connId newInternalId newInternalRcvId = DB.executeNamed dbConn [sql| @@ -582,13 +777,13 @@ updateLastIdsRcv_ dbConn connAlias newInternalId newInternalRcvId = |] [ ":last_internal_msg_id" := newInternalId, ":last_internal_rcv_msg_id" := newInternalRcvId, - ":conn_alias" := connAlias + ":conn_alias" := connId ] -- * createRcvMsg helpers -insertRcvMsgBase_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO () -insertRcvMsgBase_ dbConn connAlias RcvMsgData {..} = do +insertRcvMsgBase_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () +insertRcvMsgBase_ dbConn connId RcvMsgData {..} = do DB.executeNamed dbConn [sql| @@ -597,15 +792,15 @@ insertRcvMsgBase_ dbConn connAlias RcvMsgData {..} = do VALUES (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL,:body); |] - [ ":conn_alias" := connAlias, + [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_rcv_id" := internalRcvId, ":body" := decodeUtf8 msgBody ] -insertRcvMsgDetails_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO () -insertRcvMsgDetails_ dbConn connAlias RcvMsgData {..} = +insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () +insertRcvMsgDetails_ dbConn connId RcvMsgData {..} = DB.executeNamed dbConn [sql| @@ -618,7 +813,7 @@ insertRcvMsgDetails_ dbConn connAlias RcvMsgData {..} = :broker_id,:broker_ts,:rcv_status, NULL, NULL, :internal_hash,:external_prev_snd_hash,:integrity); |] - [ ":conn_alias" := connAlias, + [ ":conn_alias" := connId, ":internal_rcv_id" := internalRcvId, ":internal_id" := internalId, ":external_snd_id" := fst senderMeta, @@ -631,8 +826,8 @@ insertRcvMsgDetails_ dbConn connAlias RcvMsgData {..} = ":integrity" := msgIntegrity ] -updateHashRcv_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO () -updateHashRcv_ dbConn connAlias RcvMsgData {..} = +updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () +updateHashRcv_ dbConn connId RcvMsgData {..} = DB.executeNamed dbConn -- last_internal_rcv_msg_id equality check prevents race condition in case next id was reserved @@ -645,14 +840,14 @@ updateHashRcv_ dbConn connAlias RcvMsgData {..} = |] [ ":last_external_snd_msg_id" := fst senderMeta, ":last_rcv_msg_hash" := internalHash, - ":conn_alias" := connAlias, + ":conn_alias" := connId, ":last_internal_rcv_msg_id" := internalRcvId ] -- * updateSndIds helpers -retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnAlias -> IO (InternalId, InternalSndId, PrevSndMsgHash) -retrieveLastIdsAndHashSnd_ dbConn connAlias = do +retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash) +retrieveLastIdsAndHashSnd_ dbConn connId = do [(lastInternalId, lastInternalSndId, lastSndHash)] <- DB.queryNamed dbConn @@ -661,11 +856,11 @@ retrieveLastIdsAndHashSnd_ dbConn connAlias = do FROM connections WHERE conn_alias = :conn_alias; |] - [":conn_alias" := connAlias] + [":conn_alias" := connId] return (lastInternalId, lastInternalSndId, lastSndHash) -updateLastIdsSnd_ :: DB.Connection -> ConnAlias -> InternalId -> InternalSndId -> IO () -updateLastIdsSnd_ dbConn connAlias newInternalId newInternalSndId = +updateLastIdsSnd_ :: DB.Connection -> ConnId -> InternalId -> InternalSndId -> IO () +updateLastIdsSnd_ dbConn connId newInternalId newInternalSndId = DB.executeNamed dbConn [sql| @@ -676,13 +871,13 @@ updateLastIdsSnd_ dbConn connAlias newInternalId newInternalSndId = |] [ ":last_internal_msg_id" := newInternalId, ":last_internal_snd_msg_id" := newInternalSndId, - ":conn_alias" := connAlias + ":conn_alias" := connId ] -- * createSndMsg helpers -insertSndMsgBase_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO () -insertSndMsgBase_ dbConn connAlias SndMsgData {..} = do +insertSndMsgBase_ :: DB.Connection -> ConnId -> SndMsgData -> IO () +insertSndMsgBase_ dbConn connId SndMsgData {..} = do DB.executeNamed dbConn [sql| @@ -691,15 +886,15 @@ insertSndMsgBase_ dbConn connAlias SndMsgData {..} = do VALUES (:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id,:body); |] - [ ":conn_alias" := connAlias, + [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_snd_id" := internalSndId, ":body" := decodeUtf8 msgBody ] -insertSndMsgDetails_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO () -insertSndMsgDetails_ dbConn connAlias SndMsgData {..} = +insertSndMsgDetails_ :: DB.Connection -> ConnId -> SndMsgData -> IO () +insertSndMsgDetails_ dbConn connId SndMsgData {..} = DB.executeNamed dbConn [sql| @@ -708,15 +903,15 @@ insertSndMsgDetails_ dbConn connAlias SndMsgData {..} = VALUES (:conn_alias,:internal_snd_id,:internal_id,:snd_status, NULL, NULL,:internal_hash); |] - [ ":conn_alias" := connAlias, + [ ":conn_alias" := connId, ":internal_snd_id" := internalSndId, ":internal_id" := internalId, ":snd_status" := Created, ":internal_hash" := internalHash ] -updateHashSnd_ :: DB.Connection -> ConnAlias -> SndMsgData -> IO () -updateHashSnd_ dbConn connAlias SndMsgData {..} = +updateHashSnd_ :: DB.Connection -> ConnId -> SndMsgData -> IO () +updateHashSnd_ dbConn connId SndMsgData {..} = DB.executeNamed dbConn -- last_internal_snd_msg_id equality check prevents race condition in case next id was reserved @@ -727,7 +922,7 @@ updateHashSnd_ dbConn connAlias SndMsgData {..} = AND last_internal_snd_msg_id = :last_internal_snd_msg_id; |] [ ":last_snd_msg_hash" := internalHash, - ":conn_alias" := connAlias, + ":conn_alias" := connId, ":last_internal_snd_msg_id" := internalSndId ] @@ -745,10 +940,10 @@ bcastExists_ dbConn bId = not . null <$> queryBcast queryBcast :: IO [Only BroadcastId] queryBcast = DB.query dbConn "SELECT broadcast_id FROM broadcasts WHERE broadcast_id = ?;" (Only bId) -bcastConnExists_ :: DB.Connection -> BroadcastId -> ConnAlias -> IO Bool -bcastConnExists_ dbConn bId connAlias = not . null <$> queryBcastConn +bcastConnExists_ :: DB.Connection -> BroadcastId -> ConnId -> IO Bool +bcastConnExists_ dbConn bId connId = not . null <$> queryBcastConn where - queryBcastConn :: IO [(BroadcastId, ConnAlias)] + queryBcastConn :: IO [(BroadcastId, ConnId)] queryBcastConn = DB.query dbConn @@ -757,4 +952,37 @@ bcastConnExists_ dbConn bId connAlias = not . null <$> queryBcastConn FROM broadcast_connections WHERE broadcast_id = ? AND conn_alias = ?; |] - (bId, connAlias) + (bId, connId) + +-- create record with a random ID + +getConnId_ :: DB.Connection -> TVar ChaChaDRG -> ConnData -> IO (Either StoreError ConnId) +getConnId_ dbConn gVar ConnData {connId = ""} = getUniqueRandomId gVar $ getConnData_ dbConn +getConnId_ _ _ ConnData {connId} = pure $ Right connId + +getUniqueRandomId :: TVar ChaChaDRG -> (ByteString -> IO (Maybe a)) -> IO (Either StoreError ByteString) +getUniqueRandomId gVar get = tryGet 3 + where + tryGet :: Int -> IO (Either StoreError ByteString) + tryGet 0 = pure $ Left SEUniqueID + tryGet n = do + id' <- randomId gVar 12 + get id' >>= \case + Nothing -> pure $ Right id' + Just _ -> tryGet (n - 1) + +createWithRandomId :: TVar ChaChaDRG -> (ByteString -> IO ()) -> IO (Either StoreError ByteString) +createWithRandomId gVar create = tryCreate 3 + where + tryCreate :: Int -> IO (Either StoreError ByteString) + tryCreate 0 = pure $ Left SEUniqueID + tryCreate n = do + id' <- randomId gVar 12 + E.try (create id') >>= \case + Right _ -> pure $ Right id' + Left e + | DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1) + | otherwise -> pure . Left . SEInternal $ bshow e + +randomId :: TVar ChaChaDRG -> Int -> IO ByteString +randomId gVar n = encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n) diff --git a/src/Simplex/Messaging/Parsers.hs b/src/Simplex/Messaging/Parsers.hs index 2b9522e3e..5e7741c29 100644 --- a/src/Simplex/Messaging/Parsers.hs +++ b/src/Simplex/Messaging/Parsers.hs @@ -30,7 +30,7 @@ base64StringP = do pure $ str <> pad tsISO8601P :: Parser UTCTime -tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill (== ' ') +tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill wordEnd parse :: Parser a -> e -> (ByteString -> Either e a) parse parser err = first (const err) . parseAll parser @@ -42,14 +42,17 @@ parseRead :: Read a => Parser ByteString -> Parser a parseRead = (>>= maybe (fail "cannot read") pure . readMaybe . B.unpack) parseRead1 :: Read a => Parser a -parseRead1 = parseRead $ A.takeTill (== ' ') +parseRead1 = parseRead $ A.takeTill wordEnd parseRead2 :: Read a => Parser a parseRead2 = parseRead $ do - w1 <- A.takeTill (== ' ') <* A.char ' ' - w2 <- A.takeTill (== ' ') + w1 <- A.takeTill wordEnd <* A.char ' ' + w2 <- A.takeTill wordEnd pure $ w1 <> " " <> w2 +wordEnd :: Char -> Bool +wordEnd c = c == ' ' || c == '\n' + parseString :: (ByteString -> Either String a) -> (String -> a) parseString p = either error id . p . B.pack diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index a3d9d184f..57c7ad760 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -5,6 +5,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -17,6 +18,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store (InvitationId) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) import System.Timeout @@ -29,10 +31,16 @@ agentTests (ATransport t) = do describe "Establishing duplex connection" do it "should connect via one server and one agent" $ smpAgentTest2_1_1 $ testDuplexConnection t + it "should connect via one server and one agent (random IDs)" $ + smpAgentTest2_1_1 $ testDuplexConnRandomIds t it "should connect via one server and 2 agents" $ smpAgentTest2_2_1 $ testDuplexConnection t + it "should connect via one server and 2 agents (random IDs)" $ + smpAgentTest2_2_1 $ testDuplexConnRandomIds t it "should connect via 2 servers and 2 agents" $ smpAgentTest2_2_2 $ testDuplexConnection t + it "should connect via 2 servers and 2 agents (random IDs)" $ + smpAgentTest2_2_2 $ testDuplexConnRandomIds t describe "Connection subscriptions" do it "should connect via one server and one agent" $ smpAgentTest3_1_1 $ testSubscription t @@ -41,6 +49,13 @@ agentTests (ATransport t) = do describe "Broadcast" do it "should create broadcast and send messages" $ smpAgentTest3 $ testBroadcast t + it "should create broadcast and send messages (random IDs)" $ + smpAgentTest3 $ testBroadcastRandomIds t + describe "Introduction" do + it "should send and accept introduction" $ + smpAgentTest3 $ testIntroduction t + it "should send and accept introduction (random IDs)" $ + smpAgentTest3 $ testIntroductionRandomIds t type TestTransmission p = (ACorrId, ByteString, APartyCmd p) @@ -54,9 +69,13 @@ testTE (ATransmissionOrError corrId entity cmdOrErr) = Right cmd -> Right $ APartyCmd cmd Left e -> Left e +-- | receive message to handle `h` +(<#:) :: Transport c => c -> IO (TestTransmissionOrError 'Agent) +(<#:) h = testTE <$> tGet SAgent h + -- | send transmission `t` to handle `h` and get response (#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (TestTransmissionOrError 'Agent) -h #: t = tPutRaw h t >> testTE <$> tGet SAgent h +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` @@ -75,11 +94,11 @@ correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of -- | receive message to handle `h` and validate that it is the expected one (<#) :: Transport c => c -> TestTransmission' 'Agent c' -> Expectation -h <# (corrId, cAlias, cmd) = tGet SAgent h >>= (`shouldBe` (corrId, cAlias, Right (APartyCmd cmd))) . testTE +h <# (corrId, cAlias, cmd) = (h <#:) >>= (`shouldBe` (corrId, cAlias, Right (APartyCmd cmd))) -- | receive message to handle `h` and validate it using predicate `p` (<#=) :: Transport c => c -> (TestTransmission 'Agent -> Bool) -> Expectation -h <#= p = tGet SAgent h >>= (`shouldSatisfy` p . correctTransmission . testTE) +h <#= p = (h <#:) >>= (`shouldSatisfy` p . correctTransmission) -- | test that nothing is delivered to handle `h` during 10ms (#:#) :: Transport c => c -> String -> Expectation @@ -90,53 +109,75 @@ h #:# err = tryGet `shouldReturn` () Just _ -> error err _ -> return () -pattern Msg :: MsgBody -> APartyCmd 'Agent -pattern Msg msgBody <- APartyCmd MSG {msgBody, msgIntegrity = MsgOk} - pattern Sent :: AgentMsgId -> APartyCmd 'Agent pattern Sent msgId <- APartyCmd (SENT msgId) -pattern Inv :: SMPQueueInfo -> APartyCmd 'Agent -pattern Inv invitation <- APartyCmd (INV invitation) +pattern Msg :: MsgBody -> APartyCmd 'Agent +pattern Msg msgBody <- APartyCmd MSG {msgBody, msgIntegrity = MsgOk} + +pattern Inv :: SMPQueueInfo -> Either AgentErrorType (APartyCmd 'Agent) +pattern Inv invitation <- Right (APartyCmd (INV invitation)) + +pattern Req :: InvitationId -> EntityInfo -> Either AgentErrorType (APartyCmd 'Agent) +pattern Req invId eInfo <- Right (APartyCmd (REQ (IE (Conn invId)) eInfo)) testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do - ("1", "C:bob", Right (Inv qInfo)) <- alice #: ("1", "C:bob", "NEW") + ("1", "C:bob", Inv qInfo) <- alice #: ("1", "C:bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo bob #: ("11", "C:alice", "JOIN " <> qInfo') #> ("", "C:alice", CON) alice <# ("", "C:bob", CON) - alice #: ("2", "C:bob", "SEND :hello") =#> \case ("2", "C:bob", Sent 1) -> True; _ -> False - alice #: ("3", "C:bob", "SEND :how are you?") =#> \case ("3", "C:bob", Sent 2) -> True; _ -> False + alice #: ("2", "C:bob", "SEND :hello") #> ("2", "C:bob", SENT 1) + alice #: ("3", "C:bob", "SEND :how are you?") #> ("3", "C:bob", SENT 2) bob <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False bob <#= \case ("", "C:alice", Msg "how are you?") -> True; _ -> False - bob #: ("14", "C:alice", "SEND 9\nhello too") =#> \case ("14", "C:alice", Sent 3) -> True; _ -> False + bob #: ("14", "C:alice", "SEND 9\nhello too") #> ("14", "C:alice", SENT 3) alice <#= \case ("", "C:bob", Msg "hello too") -> True; _ -> False - bob #: ("15", "C:alice", "SEND 9\nmessage 1") =#> \case ("15", "C:alice", Sent 4) -> True; _ -> False + bob #: ("15", "C:alice", "SEND 9\nmessage 1") #> ("15", "C:alice", SENT 4) alice <#= \case ("", "C:bob", Msg "message 1") -> True; _ -> False alice #: ("5", "C:bob", "OFF") #> ("5", "C:bob", OK) bob #: ("17", "C:alice", "SEND 9\nmessage 3") #> ("17", "C:alice", ERR (SMP AUTH)) alice #: ("6", "C:bob", "DEL") #> ("6", "C:bob", OK) alice #:# "nothing else should be delivered to alice" +testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () +testDuplexConnRandomIds _ alice bob = do + ("1", bobConn, Inv qInfo) <- alice #: ("1", "C:", "NEW") + let qInfo' = serializeSmpQueueInfo qInfo + ("", aliceConn, Right (APartyCmd CON)) <- bob #: ("11", "C:", "JOIN " <> qInfo') + alice <# ("", bobConn, CON) + alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, SENT 1) + alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, SENT 2) + bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False + bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False + bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, SENT 3) + alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False + bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, SENT 4) + alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False + alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) + bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, ERR (SMP AUTH)) + alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) + alice #:# "nothing else should be delivered to alice" + testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do - ("1", "C:bob", Right (Inv qInfo)) <- alice1 #: ("1", "C:bob", "NEW") + ("1", "C:bob", Inv qInfo) <- alice1 #: ("1", "C:bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo bob #: ("11", "C:alice", "JOIN " <> qInfo') #> ("", "C:alice", CON) - bob #: ("12", "C:alice", "SEND 5\nhello") =#> \case ("12", "C:alice", Sent _) -> True; _ -> False - bob #: ("13", "C:alice", "SEND 11\nhello again") =#> \case ("13", "C:alice", Sent _) -> True; _ -> False + bob #: ("12", "C:alice", "SEND 5\nhello") #> ("12", "C:alice", SENT 1) + bob #: ("13", "C:alice", "SEND 11\nhello again") #> ("13", "C:alice", SENT 2) alice1 <# ("", "C:bob", CON) alice1 <#= \case ("", "C:bob", Msg "hello") -> True; _ -> False alice1 <#= \case ("", "C:bob", Msg "hello again") -> True; _ -> False alice2 #: ("21", "C:bob", "SUB") #> ("21", "C:bob", OK) alice1 <# ("", "C:bob", END) - bob #: ("14", "C:alice", "SEND 2\nhi") =#> \case ("14", "C:alice", Sent _) -> True; _ -> False + bob #: ("14", "C:alice", "SEND 2\nhi") #> ("14", "C:alice", SENT 3) alice2 <#= \case ("", "C:bob", Msg "hi") -> True; _ -> False alice1 #:# "nothing else should be delivered to alice1" testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () testSubscrNotification _ (server, _) client = do - client #: ("1", "C:conn1", "NEW") =#> \case ("1", "C:conn1", Inv _) -> True; _ -> False + client #: ("1", "C:conn1", "NEW") =#> \case ("1", "C:conn1", APartyCmd INV {}) -> True; _ -> False client #:# "nothing should be delivered to client before the server is killed" killThread server client <# ("", "C:conn1", END) @@ -156,8 +197,8 @@ testBroadcast _ alice bob tom = do alice #: ("e3", "B:team", "ADD C:unknown") #> ("e3", "B:team", ERR $ CONN NOT_FOUND) alice #: ("e4", "B:team", "ADD C:bob") #> ("e4", "B:team", ERR $ CONN DUPLICATE) -- send message - alice #: ("4", "B:team", "SEND 5\nhello") #> ("4", "C:bob", SENT 1) - alice <# ("4", "C:tom", SENT 1) + alice #: ("4", "B:team", "SEND 5\nhello") =#> \case ("4", c, Sent 1) -> c == "C:bob" || c == "C:tom"; _ -> False + alice <#= \case ("4", c, Sent 1) -> c == "C:bob" || c == "C:tom"; _ -> False alice <# ("4", "B:team", SENT 0) bob <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False tom <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False @@ -177,13 +218,104 @@ testBroadcast _ alice bob tom = do -- commands with errors alice #: ("e8", "B:team", "DEL") #> ("e8", "B:team", ERR $ BCAST B_NOT_FOUND) alice #: ("e9", "B:group", "DEL") #> ("e9", "B:group", ERR $ BCAST B_NOT_FOUND) - where - connect :: (c, ByteString) -> (c, ByteString) -> IO () - connect (h1, name1) (h2, name2) = do - ("c1", _, Right (Inv qInfo)) <- h1 #: ("c1", "C:" <> name2, "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - h2 #: ("c2", "C:" <> name1, "JOIN " <> qInfo') =#> \case ("", c1, APartyCmd CON) -> c1 == "C:" <> name1; _ -> False - h1 <#= \case ("", c2, APartyCmd CON) -> c2 == "C:" <> name2; _ -> False + +testBroadcastRandomIds :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () +testBroadcastRandomIds _ alice bob tom = do + -- establish connections + (aliceB, bobA) <- alice `connect'` bob + (aliceT, tomA) <- alice `connect'` tom + -- create and set up broadcast + ("1", team, Right (APartyCmd OK)) <- alice #: ("1", "B:", "NEW") + alice #: ("2", team, "ADD " <> bobA) #> ("2", team, OK) + alice #: ("3", team, "ADD " <> tomA) #> ("3", team, OK) + -- commands with errors + alice #: ("e1", team, "NEW") #> ("e1", team, ERR $ BCAST B_DUPLICATE) + alice #: ("e2", "B:group", "ADD " <> bobA) #> ("e2", "B:group", ERR $ BCAST B_NOT_FOUND) + alice #: ("e3", team, "ADD C:unknown") #> ("e3", team, ERR $ CONN NOT_FOUND) + alice #: ("e4", team, "ADD " <> bobA) #> ("e4", team, ERR $ CONN DUPLICATE) + -- send message + alice #: ("4", team, "SEND 5\nhello") =#> \case ("4", c, Sent 1) -> c == bobA || c == tomA; _ -> False + alice <#= \case ("4", c, Sent 1) -> c == bobA || c == tomA; _ -> False + alice <# ("4", team, SENT 0) + bob <#= \case ("", c, Msg "hello") -> c == aliceB; _ -> False + tom <#= \case ("", c, Msg "hello") -> c == aliceT; _ -> False + -- remove one connection + alice #: ("5", team, "REM " <> tomA) #> ("5", team, OK) + alice #: ("6", team, "SEND 11\nhello again") #> ("6", bobA, SENT 2) + alice <# ("6", team, SENT 0) + bob <#= \case ("", c, Msg "hello again") -> c == aliceB; _ -> False + tom #:# "nothing delivered to tom" + -- commands with errors + alice #: ("e5", "B:group", "REM " <> bobA) #> ("e5", "B:group", ERR $ BCAST B_NOT_FOUND) + alice #: ("e6", team, "REM C:unknown") #> ("e6", team, ERR $ CONN NOT_FOUND) + alice #: ("e7", team, "REM " <> tomA) #> ("e7", team, ERR $ CONN NOT_FOUND) + -- delete broadcast + alice #: ("7", team, "DEL") #> ("7", team, OK) + alice #: ("8", team, "SEND 11\ntry sending") #> ("8", team, ERR $ BCAST B_NOT_FOUND) + -- commands with errors + alice #: ("e8", team, "DEL") #> ("e8", team, ERR $ BCAST B_NOT_FOUND) + alice #: ("e9", "B:group", "DEL") #> ("e9", "B:group", ERR $ BCAST B_NOT_FOUND) + +testIntroduction :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () +testIntroduction _ alice bob tom = do + -- establish connections + (alice, "alice") `connect` (bob, "bob") + (alice, "alice") `connect` (tom, "tom") + -- send introduction of tom to bob + alice #: ("1", "C:bob", "INTRO C:tom 8\nmeet tom") #> ("1", "C:bob", OK) + ("", "C:alice", Req invId1 "meet tom") <- (bob <#:) + bob #: ("2", "C:tom_via_alice", "ACPT C:" <> invId1 <> " 7\nI'm bob") #> ("2", "C:tom_via_alice", OK) + ("", "C:alice", Req invId2 "I'm bob") <- (tom <#:) + -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter + -- or this should be another command, not ACPT + tom #: ("3", "C:bob_via_alice", "ACPT C:" <> invId2 <> " 8\ntom here") #> ("3", "C:bob_via_alice", OK) + tom <# ("", "C:bob_via_alice", CON) + bob <# ("", "C:tom_via_alice", CON) + alice <# ("", "C:bob", ICON (IE (Conn "tom"))) + -- they can message each other now + tom #: ("4", "C:bob_via_alice", "SEND :hello") #> ("4", "C:bob_via_alice", SENT 1) + bob <#= \case ("", "C:tom_via_alice", Msg "hello") -> True; _ -> False + bob #: ("5", "C:tom_via_alice", "SEND 9\nhello too") #> ("5", "C:tom_via_alice", SENT 2) + tom <#= \case ("", "C:bob_via_alice", Msg "hello too") -> True; _ -> False + +testIntroductionRandomIds :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () +testIntroductionRandomIds _ alice bob tom = do + -- establish connections + (aliceB, bobA) <- alice `connect'` bob + (aliceT, tomA) <- alice `connect'` tom + -- send introduction of tom to bob + alice #: ("1", bobA, "INTRO " <> tomA <> " 8\nmeet tom") #> ("1", bobA, OK) + ("", aliceB', Req invId1 "meet tom") <- (bob <#:) + aliceB' `shouldBe` aliceB + ("2", tomB, Right (APartyCmd OK)) <- bob #: ("2", "C:", "ACPT C:" <> invId1 <> " 7\nI'm bob") + ("", aliceT', Req invId2 "I'm bob") <- (tom <#:) + aliceT' `shouldBe` aliceT + -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter + -- or this should be another command, not ACPT + ("3", bobT, Right (APartyCmd OK)) <- tom #: ("3", "C:", "ACPT C:" <> invId2 <> " 8\ntom here") + tom <# ("", bobT, CON) + bob <# ("", tomB, CON) + alice <# ("", bobA, ICON . IE . Conn $ B.drop 2 tomA) + -- they can message each other now + tom #: ("4", bobT, "SEND :hello") #> ("4", bobT, SENT 1) + bob <#= \case ("", c, Msg "hello") -> c == tomB; _ -> False + bob #: ("5", tomB, "SEND 9\nhello too") #> ("5", tomB, SENT 2) + tom <#= \case ("", c, Msg "hello too") -> c == bobT; _ -> False + +connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () +connect (h1, name1) (h2, name2) = do + ("c1", _, Inv qInfo) <- h1 #: ("c1", "C:" <> name2, "NEW") + let qInfo' = serializeSmpQueueInfo qInfo + h2 #: ("c2", "C:" <> name1, "JOIN " <> qInfo') #> ("", "C:" <> name1, CON) + h1 <# ("", "C:" <> name2, CON) + +connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) +connect' h1 h2 = do + ("c1", conn2, Inv qInfo) <- h1 #: ("c1", "C:", "NEW") + let qInfo' = serializeSmpQueueInfo qInfo + ("", conn1, Right (APartyCmd CON)) <- h2 #: ("c2", "C:", "JOIN " <> qInfo') + h1 <# ("", conn2, CON) + pure (conn1, conn2) samplePublicKey :: ByteString samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR" @@ -205,7 +337,7 @@ syntaxTests t = do -- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided) -- TODO: add tests with defined connection alias it "using same server as in invitation" $ - ("311", "C:", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "C:", "ERR SMP AUTH") + ("311", "C:a", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "C:a", "ERR SMP AUTH") describe "invalid" do -- TODO: JOIN is not merged yet - to be added it "no parameters" $ ("321", "C:", "JOIN") >#> ("321", "C:", "ERR CMD SYNTAX") diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 834720645..2f8383a8c 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -8,9 +8,11 @@ module AgentTests.SQLiteTests (storeTests) where import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.STM (newTVarIO) import Control.Monad (replicateM_) import Control.Monad.Except (ExceptT, runExceptT) import qualified Crypto.PubKey.RSA as R +import Crypto.Random (drgNew) import Data.ByteString.Char8 (ByteString) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) @@ -73,11 +75,13 @@ storeTests = do describe "Queue and Connection management" do describe "createRcvConn" do testCreateRcvConn + testCreateRcvConnRandomId testCreateRcvConnDuplicate describe "createSndConn" do testCreateSndConn + testCreateSndConnRandomID testCreateSndConnDuplicate - describe "getAllConnAliases" testGetAllConnAliases + describe "getAllConnIds" testGetAllConnIds describe "getRcvConn" testGetRcvConn describe "deleteConn" do testDeleteRcvConn @@ -104,14 +108,16 @@ storeTests = do testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore) testConcurrentWrites = it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do - _ <- runExceptT $ createRcvConn s1 rcvQueue1 - concurrently_ (runTest s1) (runTest s2) + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 + let ConnData {connId} = cData1 + concurrently_ (runTest s1 connId) (runTest s2 connId) where - runTest :: SQLiteStore -> IO (Either StoreError ()) - runTest store = runExceptT . replicateM_ 100 $ do - (internalId, internalRcvId, _, _) <- updateRcvIds store rcvQueue1 + runTest :: SQLiteStore -> ConnId -> IO (Either StoreError ()) + runTest store connId = runExceptT . replicateM_ 100 $ do + (internalId, internalRcvId, _, _) <- updateRcvIds store connId let rcvMsgData = mkRcvMsgData internalId internalRcvId 0 "0" "hash_dummy" - createRcvMsg store rcvQueue1 rcvMsgData + createRcvMsg store connId rcvMsgData testCompiledThreadsafe :: SpecWith SQLiteStore testCompiledThreadsafe = @@ -132,12 +138,14 @@ testForeignKeysEnabled = DB.execute_ (dbConn store) inconsistentQuery `shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint) +cData1 :: ConnData +cData1 = ConnData {connId = "conn1", viaInv = Nothing, connLevel = 1} + rcvQueue1 :: RcvQueue rcvQueue1 = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "1234", - connAlias = "conn1", rcvPrivateKey = C.safePrivateKey (1, 2, 3), sndId = Just "2345", sndKey = Nothing, @@ -151,7 +159,6 @@ sndQueue1 = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "3456", - connAlias = "conn1", sndPrivateKey = C.safePrivateKey (1, 2, 3), encryptKey = C.PublicKey $ R.PublicKey 1 2 3, signKey = C.safePrivateKey (1, 2, 3), @@ -161,64 +168,95 @@ sndQueue1 = testCreateRcvConn :: SpecWith SQLiteStore testCreateRcvConn = it "should create RcvConnection and add SndQueue" $ \store -> do - createRcvConn store rcvQueue1 - `returnsResult` () + g <- newTVarIO =<< drgNew + createRcvConn store g cData1 rcvQueue1 + `returnsResult` "conn1" getConn store "conn1" - `returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1) + `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) upgradeRcvConnToDuplex store "conn1" sndQueue1 `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) + +testCreateRcvConnRandomId :: SpecWith SQLiteStore +testCreateRcvConnRandomId = + it "should create RcvConnection and add SndQueue with random ID" $ \store -> do + g <- newTVarIO =<< drgNew + Right connId <- runExceptT $ createRcvConn store g cData1 {connId = ""} rcvQueue1 + getConn store connId + `returnsResult` SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1) + upgradeRcvConnToDuplex store connId sndQueue1 + `returnsResult` () + getConn store connId + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1) testCreateRcvConnDuplicate :: SpecWith SQLiteStore testCreateRcvConnDuplicate = it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 - createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + createRcvConn store g cData1 rcvQueue1 `throwsError` SEConnDuplicate testCreateSndConn :: SpecWith SQLiteStore testCreateSndConn = it "should create SndConnection and add RcvQueue" $ \store -> do - createSndConn store sndQueue1 - `returnsResult` () + g <- newTVarIO =<< drgNew + createSndConn store g cData1 sndQueue1 + `returnsResult` "conn1" getConn store "conn1" - `returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1) + `returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1) upgradeSndConnToDuplex store "conn1" rcvQueue1 `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) + +testCreateSndConnRandomID :: SpecWith SQLiteStore +testCreateSndConnRandomID = + it "should create SndConnection and add RcvQueue with random ID" $ \store -> do + g <- newTVarIO =<< drgNew + Right connId <- runExceptT $ createSndConn store g cData1 {connId = ""} sndQueue1 + getConn store connId + `returnsResult` SomeConn SCSnd (SndConnection cData1 {connId} sndQueue1) + upgradeSndConnToDuplex store connId rcvQueue1 + `returnsResult` () + getConn store connId + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1) testCreateSndConnDuplicate :: SpecWith SQLiteStore testCreateSndConnDuplicate = it "should throw error on attempt to create duplicate SndConnection" $ \store -> do - _ <- runExceptT $ createSndConn store sndQueue1 - createSndConn store sndQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createSndConn store g cData1 sndQueue1 + createSndConn store g cData1 sndQueue1 `throwsError` SEConnDuplicate -testGetAllConnAliases :: SpecWith SQLiteStore -testGetAllConnAliases = +testGetAllConnIds :: SpecWith SQLiteStore +testGetAllConnIds = it "should get all conn aliases" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 - _ <- runExceptT $ createSndConn store sndQueue1 {connAlias = "conn2"} - getAllConnAliases store - `returnsResult` ["conn1" :: ConnAlias, "conn2" :: ConnAlias] + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createSndConn store g cData1 {connId = "conn2"} sndQueue1 + getAllConnIds store + `returnsResult` ["conn1" :: ConnId, "conn2" :: ConnId] testGetRcvConn :: SpecWith SQLiteStore testGetRcvConn = it "should get connection using rcv queue id and server" $ \store -> do let smpServer = SMPServer "smp.simplex.im" (Just "5223") testKeyHash let recipientId = "1234" - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 getRcvConn store smpServer recipientId - `returnsResult` SomeConn SCRcv (RcvConnection (connAlias (rcvQueue1 :: RcvQueue)) rcvQueue1) + `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) testDeleteRcvConn :: SpecWith SQLiteStore testDeleteRcvConn = it "should create RcvConnection and delete it" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 getConn store "conn1" - `returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1) + `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) deleteConn store "conn1" `returnsResult` () -- TODO check queues are deleted as well @@ -228,9 +266,10 @@ testDeleteRcvConn = testDeleteSndConn :: SpecWith SQLiteStore testDeleteSndConn = it "should create SndConnection and delete it" $ \store -> do - _ <- runExceptT $ createSndConn store sndQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createSndConn store g cData1 sndQueue1 getConn store "conn1" - `returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1) + `returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1) deleteConn store "conn1" `returnsResult` () -- TODO check queues are deleted as well @@ -240,10 +279,11 @@ testDeleteSndConn = testDeleteDuplexConn :: SpecWith SQLiteStore testDeleteDuplexConn = it "should create DuplexConnection and delete it" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) deleteConn store "conn1" `returnsResult` () -- TODO check queues are deleted as well @@ -253,12 +293,12 @@ testDeleteDuplexConn = testUpgradeRcvConnToDuplex :: SpecWith SQLiteStore testUpgradeRcvConnToDuplex = it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" $ \store -> do - _ <- runExceptT $ createSndConn store sndQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createSndConn store g cData1 sndQueue1 let anotherSndQueue = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "2345", - connAlias = "conn1", sndPrivateKey = C.safePrivateKey (1, 2, 3), encryptKey = C.PublicKey $ R.PublicKey 1 2 3, signKey = C.safePrivateKey (1, 2, 3), @@ -273,12 +313,12 @@ testUpgradeRcvConnToDuplex = testUpgradeSndConnToDuplex :: SpecWith SQLiteStore testUpgradeSndConnToDuplex = it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 let anotherRcvQueue = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "3456", - connAlias = "conn1", rcvPrivateKey = C.safePrivateKey (1, 2, 3), sndId = Just "4567", sndKey = Nothing, @@ -295,40 +335,43 @@ testUpgradeSndConnToDuplex = testSetRcvQueueStatus :: SpecWith SQLiteStore testSetRcvQueueStatus = it "should update status of RcvQueue" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 getConn store "conn1" - `returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1) + `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) setRcvQueueStatus store rcvQueue1 Confirmed `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1 {status = Confirmed}) + `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1 {status = Confirmed}) testSetSndQueueStatus :: SpecWith SQLiteStore testSetSndQueueStatus = it "should update status of SndQueue" $ \store -> do - _ <- runExceptT $ createSndConn store sndQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createSndConn store g cData1 sndQueue1 getConn store "conn1" - `returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1) + `returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1) setSndQueueStatus store sndQueue1 Confirmed `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1 {status = Confirmed}) + `returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1 {status = Confirmed}) testSetQueueStatusDuplex :: SpecWith SQLiteStore testSetQueueStatusDuplex = it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) setRcvQueueStatus store rcvQueue1 Secured `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1) setSndQueueStatus store sndQueue1 Confirmed `returnsResult` () getConn store "conn1" - `returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed}) + `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed}) testSetRcvQueueStatusNoQueue :: SpecWith SQLiteStore testSetRcvQueueStatusNoQueue = @@ -362,20 +405,22 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash = msgIntegrity = MsgOk } -testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> RcvQueue -> RcvMsgData -> Expectation -testCreateRcvMsg' store expectedPrevSndId expectedPrevHash rcvQueue rcvMsgData@RcvMsgData {..} = do - updateRcvIds store rcvQueue +testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation +testCreateRcvMsg' st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do + updateRcvIds st connId `returnsResult` (internalId, internalRcvId, expectedPrevSndId, expectedPrevHash) - createRcvMsg store rcvQueue rcvMsgData + createRcvMsg st connId rcvMsgData `returnsResult` () testCreateRcvMsg :: SpecWith SQLiteStore testCreateRcvMsg = - it "should reserve internal ids and create a RcvMsg" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + it "should reserve internal ids and create a RcvMsg" $ \st -> do + g <- newTVarIO =<< drgNew + let ConnData {connId} = cData1 + _ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 -- TODO getMsg to check message - testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy" - testCreateRcvMsg' store 1 "hash_dummy" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy" + testCreateRcvMsg' st 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy" + testCreateRcvMsg' st 1 "hash_dummy" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy" mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData mkSndMsgData internalId internalSndId internalHash = @@ -387,29 +432,33 @@ mkSndMsgData internalId internalSndId internalHash = internalHash } -testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> SndQueue -> SndMsgData -> Expectation -testCreateSndMsg' store expectedPrevHash sndQueue sndMsgData@SndMsgData {..} = do - updateSndIds store sndQueue +testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation +testCreateSndMsg' store expectedPrevHash connId sndMsgData@SndMsgData {..} = do + updateSndIds store connId `returnsResult` (internalId, internalSndId, expectedPrevHash) - createSndMsg store sndQueue sndMsgData + createSndMsg store connId sndMsgData `returnsResult` () testCreateSndMsg :: SpecWith SQLiteStore testCreateSndMsg = it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \store -> do - _ <- runExceptT $ createSndConn store sndQueue1 + g <- newTVarIO =<< drgNew + let ConnData {connId} = cData1 + _ <- runExceptT $ createSndConn store g cData1 sndQueue1 -- TODO getMsg to check message - testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy" - testCreateSndMsg' store "hash_dummy" sndQueue1 $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy" + testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy" + testCreateSndMsg' store "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy" testCreateRcvAndSndMsgs :: SpecWith SQLiteStore testCreateRcvAndSndMsgs = it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do - _ <- runExceptT $ createRcvConn store rcvQueue1 + g <- newTVarIO =<< drgNew + let ConnData {connId} = cData1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 - testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1" - testCreateRcvMsg' store 1 "rcv_hash_1" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2" - testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1" - testCreateRcvMsg' store 2 "rcv_hash_2" rcvQueue1 $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3" - testCreateSndMsg' store "snd_hash_1" sndQueue1 $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2" - testCreateSndMsg' store "snd_hash_2" sndQueue1 $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3" + testCreateRcvMsg' store 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1" + testCreateRcvMsg' store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2" + testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1" + testCreateRcvMsg' store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3" + testCreateSndMsg' store "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2" + testCreateSndMsg' store "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3" diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 918b276f0..fbbfd7ccb 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -54,12 +54,12 @@ testDB3 = "tests/tmp/smp-agent3.test.protocol.db" smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> tGetRaw h -runSmpAgentTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => (c -> m a) -> m a +runSmpAgentTest :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => (c -> m a) -> m a runSmpAgentTest test = withSmpServer t . withSmpAgent t $ testSMPAgentClient test where t = transport @c -runSmpAgentServerTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> c -> m a) -> m a +runSmpAgentServerTest :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> c -> m a) -> m a runSmpAgentServerTest test = withSmpServerThreadOn t testPort $ \server -> withSmpAgentThreadOn t (agentTestPort, testPort, testDB) $ @@ -70,7 +70,7 @@ runSmpAgentServerTest test = smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> IO ()) -> Expectation smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` () -runSmpAgentTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a +runSmpAgentTestN :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a runSmpAgentTestN agents test = withSmpServer t $ run agents [] where run :: [(ServiceName, ServiceName, String)] -> [c] -> m a @@ -78,7 +78,7 @@ runSmpAgentTestN agents test = withSmpServer t $ run agents [] run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs) t = transport @c -runSmpAgentTestN_1 :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => Int -> ([c] -> m a) -> m a +runSmpAgentTestN_1 :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => Int -> ([c] -> m a) -> m a runSmpAgentTestN_1 nClients test = withSmpServer t . withSmpAgent t $ run nClients [] where run :: Int -> [c] -> m a @@ -156,17 +156,17 @@ cfg = } } -withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a +withSmpAgentThreadOn :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a withSmpAgentThreadOn t (port', smpPort', db') = let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" (Just smpPort') testKeyHash]} in serverBracket (\started -> runSMPAgentBlocking t started cfg') (removeFile db') -withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a +withSmpAgentOn :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const -withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a +withSmpAgent :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB) testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a From bf5561c89c7c1b0861c71541e565db79d92ff8b8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 12 Jun 2021 20:01:36 +0100 Subject: [PATCH 02/29] remove entities, remove broadcasts (#158) --- migrations/20210529_broadcasts.sql | 9 - src/Simplex/Messaging/Agent.hs | 272 +++++------- src/Simplex/Messaging/Agent/Client.hs | 22 +- src/Simplex/Messaging/Agent/Protocol.hs | 435 ++++---------------- src/Simplex/Messaging/Agent/Store.hs | 21 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 104 +---- tests/AgentTests.hs | 243 ++++------- 7 files changed, 295 insertions(+), 811 deletions(-) delete mode 100644 migrations/20210529_broadcasts.sql diff --git a/migrations/20210529_broadcasts.sql b/migrations/20210529_broadcasts.sql deleted file mode 100644 index 91cba0eb6..000000000 --- a/migrations/20210529_broadcasts.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE broadcasts ( - broadcast_id BLOB NOT NULL PRIMARY KEY -) WITHOUT ROWID; - -CREATE TABLE broadcast_connections ( - broadcast_id BLOB NOT NULL REFERENCES broadcasts (broadcast_id) ON DELETE CASCADE, - conn_alias BLOB NOT NULL REFERENCES connections (conn_alias), - PRIMARY KEY (broadcast_id, conn_alias) -) WITHOUT ROWID; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index e29ea41b0..d2faf3774 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -109,10 +109,10 @@ receive h c@AgentClient {rcvQ, sndQ} = forever loop where loop :: m () loop = do - ATransmissionOrError corrId entity cmdOrErr <- tGet SClient h + (corrId, connId, cmdOrErr) <- tGet SClient h case cmdOrErr of - Right cmd -> write rcvQ $ ATransmission corrId entity cmd - Left e -> write sndQ $ ATransmission corrId entity $ ERR e + Right cmd -> write rcvQ (corrId, connId, cmd) + Left e -> write sndQ (corrId, connId, ERR e) write :: TBQueue (ATransmission p) -> ATransmission p -> m () write q t = do logClient c "-->" t @@ -125,17 +125,17 @@ send h c@AgentClient {sndQ} = forever $ do logClient c "<--" t logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m () -logClient AgentClient {clientId} dir (ATransmission corrId entity cmd) = do - logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, serializeEntity entity, B.takeWhile (/= ' ') $ serializeCommand cmd] +logClient AgentClient {clientId} dir (corrId, connId, cmd) = do + logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd] client :: forall m. (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () client c@AgentClient {rcvQ, sndQ} st = forever loop where loop :: m () loop = do - t@(ATransmission corrId entity _) <- atomically $ readTBQueue rcvQ + t@(corrId, connId, _) <- atomically $ readTBQueue rcvQ runExceptT (processCommand c st t) >>= \case - Left e -> atomically . writeTBQueue sndQ $ ATransmission corrId entity (ERR e) + Left e -> atomically $ writeTBQueue sndQ (corrId, connId, ERR e) Right _ -> pure () withStore :: @@ -157,36 +157,21 @@ withStore action = do SEConnDuplicate -> CONN DUPLICATE SEBadConnType CRcv -> CONN SIMPLEX SEBadConnType CSnd -> CONN SIMPLEX - SEBcastNotFound -> BCAST B_NOT_FOUND - SEBcastDuplicate -> BCAST B_DUPLICATE e -> INTERNAL $ show e processCommand :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ATransmission 'Client -> m () -processCommand c st (ATransmission corrId entity cmd) = process c st corrId entity cmd - where - process = case entity of - Conn _ -> processConnCommand - Broadcast _ -> processBroadcastCommand - _ -> unsupportedEntity - -unsupportedEntity :: AgentMonad m => AgentClient -> SQLiteStore -> ACorrId -> Entity t -> ACommand 'Client c -> m () -unsupportedEntity c _ corrId entity _ = - atomically . writeTBQueue (sndQ c) . ATransmission corrId entity . ERR $ CMD UNSUPPORTED - -processConnCommand :: - forall c m. (AgentMonad m, EntityCommand 'Conn_ c) => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Conn_ -> ACommand 'Client c -> m () -processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case +processCommand c@AgentClient {sndQ} st (corrId, connId, cmd) = case cmd of NEW -> createNewConnection Nothing 0 >>= uncurry respond JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode Nothing 0 >> pure () -- >>= (`respond` OK) - INTRO reEntity reInfo -> makeIntroduction reEntity reInfo - ACPT inv eInfo -> acceptInvitation inv eInfo - SUB -> subscribeConnection conn + INTRO reConnId reInfo -> makeIntroduction reConnId reInfo + ACPT invId connInfo -> acceptInvitation invId connInfo + SUB -> subscribeConnection connId SUBALL -> subscribeAll - SEND msgBody -> sendClientMessage c st corrId conn msgBody - OFF -> suspendConnection conn - DEL -> deleteConnection conn + SEND msgBody -> sendMessage msgBody + OFF -> suspendConnection + DEL -> deleteConnection where - createNewConnection :: Maybe InvitationId -> Int -> m (Entity 'Conn_, ACommand 'Agent 'INV_) + createNewConnection :: Maybe InvitationId -> Int -> m (ConnId, ACommand 'Agent) createNewConnection viaInv connLevel = do -- TODO create connection alias if not passed -- make connId Maybe? @@ -196,7 +181,7 @@ processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case let cData = ConnData {connId, viaInv, connLevel} connId' <- withStore $ createRcvConn st g cData rq addSubscription c rq connId' - pure (Conn connId', INV qInfo) + pure (connId', INV qInfo) getSMPServer :: m SMPServer getSMPServer = @@ -207,7 +192,7 @@ processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1) pure $ servers L.!! i - joinConnection :: SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m (Entity 'Conn_) + joinConnection :: SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m ConnId joinConnection qInfo (ReplyMode replyMode) viaInv connLevel = do (sq, senderKey, verifyKey) <- newSendQueue qInfo g <- asks idsDrg @@ -215,73 +200,95 @@ processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case connId' <- withStore $ createSndConn st g cData sq connectToSendQueue c st sq senderKey verifyKey when (replyMode == On) $ createReplyQueue connId' sq - pure $ Conn connId' + pure connId' - makeIntroduction :: IntroEntity -> EntityInfo -> m () - makeIntroduction (IE reEntity) reInfo = case reEntity of - Conn reConn -> - withStore ((,) <$> getConn st connId <*> getConn st reConn) >>= \case - (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do - g <- asks idsDrg - introId <- withStore $ createIntro st g NewIntroduction {toConn = connId, reConn, reInfo} - sendControlMessage c sq $ A_INTRO (IE (Conn introId)) reInfo - respond conn OK - _ -> throwError $ CONN SIMPLEX - _ -> throwError $ CMD UNSUPPORTED + makeIntroduction :: IntroId -> ConnInfo -> m () + makeIntroduction reConn reInfo = + withStore ((,) <$> getConn st connId <*> getConn st reConn) >>= \case + (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do + g <- asks idsDrg + introId <- withStore $ createIntro st g NewIntroduction {toConn = connId, reConn, reInfo} + sendControlMessage c sq $ A_INTRO introId reInfo + respond connId OK + _ -> throwError $ CONN SIMPLEX - acceptInvitation :: IntroEntity -> EntityInfo -> m () - acceptInvitation (IE invEntity) eInfo = case invEntity of - Conn invId -> do - withStore (getInvitation st invId) >>= \case - Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> - withStore (getConn st viaConn) >>= \case - SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of - Nothing -> do - (conn', INV qInfo') <- createNewConnection (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId $ fromConn conn' - sendControlMessage c sq $ A_INV (Conn externalIntroId) qInfo' eInfo - respond conn' OK - Just qInfo' -> do - conn' <- joinConnection qInfo' (ReplyMode On) (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId $ fromConn conn' - respond conn' OK - _ -> throwError $ CONN SIMPLEX - _ -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD UNSUPPORTED + acceptInvitation :: InvitationId -> ConnInfo -> m () + acceptInvitation invId connInfo = + withStore (getInvitation st invId) >>= \case + Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> + withStore (getConn st viaConn) >>= \case + SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of + Nothing -> do + (connId', INV qInfo') <- createNewConnection (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId connId' + sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo + respond connId' OK + Just qInfo' -> do + connId' <- joinConnection qInfo' (ReplyMode On) (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId connId' + respond connId' OK + _ -> throwError $ CONN SIMPLEX + _ -> throwError $ CMD PROHIBITED - subscribeConnection :: Entity 'Conn_ -> m () - subscribeConnection conn'@(Conn cId) = + subscribeConnection :: ConnId -> m () + subscribeConnection cId = withStore (getConn st cId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> subscribe rq SomeConn _ (RcvConnection _ rq) -> subscribe rq _ -> throwError $ CONN SIMPLEX where - subscribe rq = subscribeQueue c rq cId >> respond conn' OK + subscribe rq = subscribeQueue c rq cId >> respond cId OK -- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct subscribeAll :: m () - subscribeAll = withStore (getAllConnIds st) >>= mapM_ (subscribeConnection . Conn) + subscribeAll = withStore (getAllConnIds st) >>= mapM_ subscribeConnection - suspendConnection :: Entity 'Conn_ -> m () - suspendConnection (Conn cId) = - withStore (getConn st cId) >>= \case + sendMessage :: MsgBody -> m () + sendMessage msgBody = + withStore (getConn st connId) >>= \case + SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq + SomeConn _ (SndConnection _ sq) -> sendMsg sq + _ -> throwError $ CONN SIMPLEX + where + sendMsg :: SndQueue -> m () + sendMsg sq = do + internalTs <- liftIO getCurrentTime + (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st connId + let msgStr = + serializeSMPMessage + SMPMessage + { senderMsgId = unSndId internalSndId, + senderTimestamp = internalTs, + previousMsgHash, + agentMessage = A_MSG msgBody + } + msgHash = C.sha256Hash msgStr + withStore $ + createSndMsg st connId $ + SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} + sendAgentMessage c sq msgStr + atomically $ writeTBQueue sndQ (corrId, connId, SENT $ unId internalId) + + suspendConnection :: m () + suspendConnection = + withStore (getConn st connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> suspend rq SomeConn _ (RcvConnection _ rq) -> suspend rq _ -> throwError $ CONN SIMPLEX where - suspend rq = suspendQueue c rq >> respond conn OK + suspend rq = suspendQueue c rq >> respond connId OK - deleteConnection :: Entity 'Conn_ -> m () - deleteConnection (Conn cId) = - withStore (getConn st cId) >>= \case + deleteConnection :: m () + deleteConnection = + withStore (getConn st connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> delete rq SomeConn _ (RcvConnection _ rq) -> delete rq _ -> delConn where - delConn = withStore (deleteConn st cId) >> respond conn OK + delConn = withStore (deleteConn st connId) >> respond connId OK delete rq = do deleteQueue c rq - removeSubscription c cId + removeSubscription c connId delConn createReplyQueue :: ConnId -> SndQueue -> m () @@ -292,8 +299,8 @@ processConnCommand c@AgentClient {sndQ} st corrId conn@(Conn connId) = \case withStore $ upgradeSndConnToDuplex st cId rq sendControlMessage c sq $ REPLY qInfo - respond :: EntityCommand t c' => Entity t -> ACommand 'Agent c' -> m () - respond ent resp = atomically . writeTBQueue sndQ $ ATransmission corrId ent resp + respond :: ConnId -> ACommand 'Agent -> m () + respond cId resp = atomically . writeTBQueue sndQ $ (corrId, cId, resp) sendControlMessage :: AgentMonad m => AgentClient -> SndQueue -> AMessage -> m () sendControlMessage c sq agentMessage = do @@ -306,57 +313,6 @@ sendControlMessage c sq agentMessage = do agentMessage } -sendClientMessage :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Conn_ -> MsgBody -> m () -sendClientMessage c st corrId (Conn cId) msgBody = - withStore (getConn st cId) >>= \case - SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq - SomeConn _ (SndConnection _ sq) -> sendMsg sq - _ -> throwError $ CONN SIMPLEX - where - sendMsg :: SndQueue -> m () - sendMsg sq = do - internalTs <- liftIO getCurrentTime - (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st cId - let msgStr = - serializeSMPMessage - SMPMessage - { senderMsgId = unSndId internalSndId, - senderTimestamp = internalTs, - previousMsgHash, - agentMessage = A_MSG msgBody - } - msgHash = C.sha256Hash msgStr - withStore $ - createSndMsg st cId $ - SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} - sendAgentMessage c sq msgStr - atomically . writeTBQueue (sndQ c) $ ATransmission corrId (Conn cId) $ SENT (unId internalId) - -processBroadcastCommand :: - forall c m. (AgentMonad m, EntityCommand 'Broadcast_ c) => AgentClient -> SQLiteStore -> ACorrId -> Entity 'Broadcast_ -> ACommand 'Client c -> m () -processBroadcastCommand c st corrId bcast@(Broadcast bId) = \case - NEW -> createNewBroadcast - ADD (Conn cId) -> withStore (addBcastConn st bId cId) >> ok - REM (Conn cId) -> withStore (removeBcastConn st bId cId) >> ok - LS -> withStore (getBcast st bId) >>= respond bcast . MS . map Conn - SEND msgBody -> withStore (getBcast st bId) >>= mapM_ (sendMsg msgBody) >> respond bcast (SENT 0) - DEL -> withStore (deleteBcast st bId) >> ok - where - createNewBroadcast :: m () - createNewBroadcast = do - g <- asks idsDrg - bId' <- withStore $ createBcast st g bId - respond (Broadcast bId') OK - - sendMsg :: MsgBody -> ConnId -> m () - sendMsg msgBody cId = sendClientMessage c st corrId (Conn cId) msgBody - - ok :: m () - ok = respond bcast OK - - respond :: EntityCommand t c' => Entity t -> ACommand 'Agent c' -> m () - respond ent resp = atomically . writeTBQueue (sndQ c) $ ATransmission corrId ent resp - subscriber :: (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () subscriber c@AgentClient {msgQ} st = forever $ do -- TODO this will only process messages and notifications @@ -370,7 +326,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do withStore (getRcvConn st srv rId) >>= \case SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq - _ -> atomically . writeTBQueue sndQ $ ATransmission "" (Conn "") (ERR $ CONN SIMPLEX) + _ -> atomically $ writeTBQueue sndQ ("", "", ERR $ CONN NOT_FOUND) where processSMP :: SConnType c -> ConnData -> RcvQueue -> m () processSMP cType ConnData {connId} rq@RcvQueue {status} = @@ -387,10 +343,10 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do HELLO verifyKey _ -> helloMsg verifyKey msgBody REPLY qInfo -> replyMsg qInfo A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash - A_INTRO entity eInfo -> introMsg entity eInfo - A_INV conn qInfo cInfo -> invMsg conn qInfo cInfo - A_REQ conn qInfo cInfo -> reqMsg conn qInfo cInfo - A_CON conn -> conMsg conn + A_INTRO introId cInfo -> introMsg introId cInfo + A_INV introId qInfo cInfo -> invMsg introId qInfo cInfo + A_REQ introId qInfo cInfo -> reqMsg introId qInfo cInfo + A_CON introId -> conMsg introId sendAck c rq return () SMP.END -> do @@ -401,8 +357,8 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do logServer "<--" c srv rId $ "unexpected: " <> bshow cmd notify . ERR $ BROKER UNEXPECTED where - notify :: EntityCommand 'Conn_ c => ACommand 'Agent c -> m () - notify msg = atomically . writeTBQueue sndQ $ ATransmission "" (Conn connId) msg + notify :: ACommand 'Agent -> m () + notify msg = atomically $ writeTBQueue sndQ ("", connId, msg) prohibited :: m () prohibited = notify . ERR $ AGENT A_PROHIBITED @@ -448,19 +404,19 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do withStore (getConnInvitation st connId) >>= \case Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do withStore $ setInvitationStatus st invId InvCon - sendControlMessage c sq $ A_CON (Conn externalIntroId) + sendControlMessage c sq $ A_CON externalIntroId _ -> pure () notify CON - introMsg :: IntroEntity -> EntityInfo -> m () - introMsg (IE entity) entityInfo = do + introMsg :: IntroId -> ConnInfo -> m () + introMsg introId reInfo = do logServer "<--" c srv rId "MSG " - case (cType, entity) of - (SCDuplex, intro@Conn {}) -> createInv intro Nothing entityInfo + case cType of + SCDuplex -> createInv introId Nothing reInfo _ -> prohibited - invMsg :: Entity 'Conn_ -> SMPQueueInfo -> EntityInfo -> m () - invMsg (Conn introId) qInfo toInfo = do + invMsg :: IntroId -> SMPQueueInfo -> ConnInfo -> m () + invMsg introId qInfo toInfo = do logServer "<--" c srv rId "MSG " case cType of SCDuplex -> @@ -470,43 +426,41 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do | otherwise -> withStore (addIntroInvitation st introId toInfo qInfo >> getConn st reConn) >>= \case SomeConn _ (DuplexConnection _ _ sq) -> do - sendControlMessage c sq $ A_REQ (Conn introId) qInfo toInfo + sendControlMessage c sq $ A_REQ introId qInfo toInfo withStore $ setIntroReStatus st introId IntroInv _ -> prohibited _ -> prohibited _ -> prohibited - reqMsg :: Entity 'Conn_ -> SMPQueueInfo -> EntityInfo -> m () - reqMsg intro qInfo connInfo = do + reqMsg :: IntroId -> SMPQueueInfo -> ConnInfo -> m () + reqMsg introId qInfo connInfo = do logServer "<--" c srv rId "MSG " case cType of - SCDuplex -> createInv intro (Just qInfo) connInfo + SCDuplex -> createInv introId (Just qInfo) connInfo _ -> prohibited - createInv :: Entity 'Conn_ -> Maybe SMPQueueInfo -> EntityInfo -> m () - createInv (Conn externalIntroId) qInfo entityInfo = do + createInv :: IntroId -> Maybe SMPQueueInfo -> ConnInfo -> m () + createInv externalIntroId qInfo connInfo = do g <- asks idsDrg - let newInv = NewInvitation {viaConn = connId, externalIntroId, entityInfo, qInfo} + let newInv = NewInvitation {viaConn = connId, externalIntroId, connInfo, qInfo} invId <- withStore $ createInvitation st g newInv - notify $ REQ (IE (Conn invId)) entityInfo + notify $ REQ invId connInfo - conMsg :: Entity 'Conn_ -> m () - conMsg (Conn introId) = do + conMsg :: IntroId -> m () + conMsg introId = do logServer "<--" c srv rId "MSG " withStore (getIntro st introId) >>= \case Introduction {toConn, toStatus, reConn, reStatus} | toConn == connId && toStatus == IntroInv -> do withStore $ setIntroToStatus st introId IntroCon - sendConMsg toConn reConn reStatus + when (reStatus == IntroCon) $ sendConMsg toConn reConn | reConn == connId && reStatus == IntroInv -> do withStore $ setIntroReStatus st introId IntroCon - sendConMsg toConn reConn toStatus + when (toStatus == IntroCon) $ sendConMsg toConn reConn | otherwise -> prohibited where - sendConMsg :: ConnId -> ConnId -> IntroStatus -> m () - sendConMsg toConn reConn IntroCon = - atomically . writeTBQueue sndQ $ ATransmission "" (Conn toConn) $ ICON $ IE (Conn reConn) - sendConMsg _ _ _ = pure () + sendConMsg :: ConnId -> ConnId -> m () + sendConMsg toConn reConn = atomically $ writeTBQueue sndQ ("", toConn, ICON reConn) agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () agentClientMsg receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index f440a6df9..c61328edf 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -119,7 +119,7 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = deleteKeys ks m = S.foldr' M.delete m ks notifySub :: ConnId -> IO () - notifySub connAlias = atomically . writeTBQueue (sndQ c) $ ATransmission "" (Conn connAlias) END + notifySub connId = atomically $ writeTBQueue (sndQ c) ("", connId, END) closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m () closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient @@ -181,31 +181,31 @@ newReceiveQueue c srv = do return (rq, SMPQueueInfo srv sId encryptKey) subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m () -subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do +subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do withLogSMP c server rcvId "SUB" $ \smp -> subscribeSMPQueue smp rcvPrivateKey rcvId - addSubscription c rq connAlias + addSubscription c rq connId addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m () -addSubscription c RcvQueue {server} connAlias = atomically $ do - modifyTVar (subscrConns c) $ M.insert connAlias server +addSubscription c RcvQueue {server} connId = atomically $ do + modifyTVar (subscrConns c) $ M.insert connId server modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server where addSub :: Maybe (Set ConnId) -> Set ConnId - addSub (Just cs) = S.insert connAlias cs - addSub _ = S.singleton connAlias + addSub (Just cs) = S.insert connId cs + addSub _ = S.singleton connId removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m () -removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do +removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ do cs <- readTVar subscrConns - writeTVar subscrConns $ M.delete connAlias cs + writeTVar subscrConns $ M.delete connId cs mapM_ (modifyTVar subscrSrvrs . M.alter (>>= delSub)) - (M.lookup connAlias cs) + (M.lookup connId cs) where delSub :: Set ConnId -> Maybe (Set ConnId) delSub cs = - let cs' = S.delete connAlias cs + let cs' = S.delete connId cs in if S.null cs' then Nothing else Just cs' logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m () diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 16f579504..4e0131e52 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -10,9 +10,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} @@ -30,17 +28,9 @@ -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md module Simplex.Messaging.Agent.Protocol ( -- * SMP agent protocol types - Entity (..), - EntityTag (..), - AnEntity (..), - IntroEntity (..), - EntityInfo, - EntityCommand, - entityCommand, + ConnInfo, ACommand (..), - ACmdTag (..), AParty (..), - APartyCmd (..), SAParty (..), SMPMessage (..), AMessage (..), @@ -49,13 +39,14 @@ module Simplex.Messaging.Agent.Protocol AgentErrorType (..), CommandErrorType (..), ConnectionErrorType (..), - BroadcastErrorType (..), BrokerErrorType (..), SMPAgentError (..), - ATransmission (..), - ATransmissionOrError (..), + ATransmission, + ATransmissionOrError, ARawTransmission, ConnId, + IntroId, + InvitationId, ReplyMode (..), AckMode (..), OnOff (..), @@ -71,14 +62,12 @@ module Simplex.Messaging.Agent.Protocol -- * Parse and serialize serializeCommand, - serializeEntity, serializeSMPMessage, serializeMsgIntegrity, serializeServer, serializeSmpQueueInfo, serializeAgentError, commandP, - anEntityP, parseSMPMessage, smpServerP, smpQueueInfoP, @@ -100,18 +89,15 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Constraint (Dict (..)) import Data.Functor (($>)) import Data.Int (Int64) -import Data.Kind (Constraint, Type) -import Data.Maybe (isJust) +import Data.Kind (Type) import Data.String (IsString (..)) import Data.Time.Clock (UTCTime) import Data.Time.ISO8601 import Data.Type.Equality import Data.Typeable () import GHC.Generics (Generic) -import GHC.TypeLits (ErrorMessage (..), TypeError) import Generic.Random (genericArbitraryU) import Network.Socket (HostName, ServiceName) import qualified Simplex.Messaging.Crypto as C @@ -133,12 +119,10 @@ import UnliftIO.Exception type ARawTransmission = (ByteString, ByteString, ByteString) -- | Parsed SMP agent protocol transmission. -data ATransmission p = forall t c. EntityCommand t c => ATransmission ACorrId (Entity t) (ACommand p c) +type ATransmission p = (ACorrId, ConnId, ACommand p) -- | SMP agent protocol transmission or transmission error. -data ATransmissionOrError p = forall t c. EntityCommand t c => ATransmissionOrError ACorrId (Entity t) (Either AgentErrorType (ACommand p c)) - -deriving instance Show (ATransmissionOrError p) +type ATransmissionOrError p = (ACorrId, ConnId, Either AgentErrorType (ACommand p)) type ACorrId = ByteString @@ -160,173 +144,29 @@ instance TestEquality SAParty where testEquality SClient SClient = Just Refl testEquality _ _ = Nothing --- | SMP agent protocol entity types -data EntityTag = Conn_ | OpenConn_ | Broadcast_ | AGroup_ - -data Entity :: EntityTag -> Type where - Conn :: {fromConn :: ByteString} -> Entity Conn_ - OpenConn :: {fromOpenConn :: ByteString} -> Entity OpenConn_ - Broadcast :: {fromBroadcast :: ByteString} -> Entity Broadcast_ - AGroup :: {fromAGroup :: ByteString} -> Entity AGroup_ - -deriving instance Eq (Entity t) - -deriving instance Show (Entity t) - -instance TestEquality Entity where - testEquality (Conn c) (Conn c') = refl c c' - testEquality (OpenConn c) (OpenConn c') = refl c c' - testEquality (Broadcast c) (Broadcast c') = refl c c' - testEquality (AGroup c) (AGroup c') = refl c c' - testEquality _ _ = Nothing - -entityId :: Entity t -> ByteString -entityId = \case - Conn bs -> bs - OpenConn bs -> bs - Broadcast bs -> bs - AGroup bs -> bs - -data AnEntity = forall t. AE (Entity t) - -data ACmd = forall (p :: AParty) (c :: ACmdTag). ACmd (SAParty p) (ACommand p c) +data ACmd = forall p. ACmd (SAParty p) (ACommand p) deriving instance Show ACmd -data APartyCmd (p :: AParty) = forall c. APartyCmd (ACommand p c) - -instance Eq (APartyCmd p) where - APartyCmd c1 == APartyCmd c2 = isJust $ testEquality c1 c2 - -deriving instance Show (APartyCmd p) - -type family EntityCommand (t :: EntityTag) (c :: ACmdTag) :: Constraint where - EntityCommand Conn_ NEW_ = () - EntityCommand Conn_ INV_ = () - EntityCommand Conn_ JOIN_ = () - EntityCommand Conn_ INTRO_ = () - EntityCommand Conn_ REQ_ = () - EntityCommand Conn_ ACPT_ = () - EntityCommand Conn_ CON_ = () - EntityCommand Conn_ ICON_ = () - EntityCommand Conn_ SUB_ = () - EntityCommand Conn_ SUBALL_ = () - EntityCommand Conn_ END_ = () - EntityCommand Conn_ SEND_ = () - EntityCommand Conn_ SENT_ = () - EntityCommand Conn_ MSG_ = () - EntityCommand Conn_ OFF_ = () - EntityCommand Conn_ DEL_ = () - EntityCommand Conn_ OK_ = () - EntityCommand Conn_ ERR_ = () - EntityCommand Broadcast_ NEW_ = () - EntityCommand Broadcast_ ADD_ = () - EntityCommand Broadcast_ REM_ = () - EntityCommand Broadcast_ LS_ = () - EntityCommand Broadcast_ MS_ = () - EntityCommand Broadcast_ SEND_ = () - EntityCommand Broadcast_ SENT_ = () - EntityCommand Broadcast_ DEL_ = () - EntityCommand Broadcast_ OK_ = () - EntityCommand Broadcast_ ERR_ = () - EntityCommand _ ERR_ = () - EntityCommand t c = - (Int ~ Bool, TypeError (Text "Entity " :<>: ShowType t :<>: Text " does not support command " :<>: ShowType c)) - -entityCommand :: Entity t -> ACommand p c -> Maybe (Dict (EntityCommand t c)) -entityCommand = \case - Conn _ -> \case - NEW -> Just Dict - INV _ -> Just Dict - JOIN {} -> Just Dict - INTRO {} -> Just Dict - REQ {} -> Just Dict - ACPT {} -> Just Dict - CON -> Just Dict - ICON {} -> Just Dict - SUB -> Just Dict - SUBALL -> Just Dict - END -> Just Dict - SEND _ -> Just Dict - SENT _ -> Just Dict - MSG {} -> Just Dict - OFF -> Just Dict - DEL -> Just Dict - OK -> Just Dict - ERR _ -> Just Dict - _ -> Nothing - Broadcast _ -> \case - NEW -> Just Dict - ADD _ -> Just Dict - REM _ -> Just Dict - LS -> Just Dict - MS _ -> Just Dict - SEND _ -> Just Dict - SENT _ -> Just Dict - DEL -> Just Dict - OK -> Just Dict - ERR _ -> Just Dict - _ -> Nothing - _ -> \case - ERR _ -> Just Dict - _ -> Nothing - -data ACmdTag - = NEW_ - | INV_ - | JOIN_ - | INTRO_ - | REQ_ - | ACPT_ - | CON_ - | ICON_ - | SUB_ - | SUBALL_ - | END_ - | SEND_ - | SENT_ - | MSG_ - | OFF_ - | DEL_ - | ADD_ - | REM_ - | LS_ - | MS_ - | OK_ - | ERR_ - -type family Introduction (t :: EntityTag) :: Constraint where - Introduction Conn_ = () - Introduction OpenConn_ = () - Introduction AGroup_ = () - Introduction t = (Int ~ Bool, TypeError (Text "Entity " :<>: ShowType t :<>: Text " cannot be INTRO'd to")) - -data IntroEntity = forall t. Introduction t => IE (Entity t) - -instance Eq IntroEntity where - IE e1 == IE e2 = isJust $ testEquality e1 e2 - -deriving instance Show IntroEntity - -type EntityInfo = ByteString +type ConnInfo = ByteString -- | Parameterized type for SMP agent protocol commands and responses from all participants. -data ACommand (p :: AParty) (c :: ACmdTag) where - NEW :: ACommand Client NEW_ -- response INV - INV :: SMPQueueInfo -> ACommand Agent INV_ - JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client JOIN_ -- response OK - INTRO :: IntroEntity -> EntityInfo -> ACommand Client INTRO_ - REQ :: IntroEntity -> EntityInfo -> ACommand Agent INTRO_ - ACPT :: IntroEntity -> EntityInfo -> ACommand Client ACPT_ - CON :: ACommand Agent CON_ -- notification that connection is established - ICON :: IntroEntity -> ACommand Agent ICON_ - SUB :: ACommand Client SUB_ - SUBALL :: ACommand Client SUBALL_ -- TODO should be moved to chat protocol - hack for subscribing to all - END :: ACommand Agent END_ +data ACommand (p :: AParty) where + NEW :: ACommand Client -- response INV + INV :: SMPQueueInfo -> ACommand Agent + JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client -- response OK + INTRO :: ConnId -> ConnInfo -> ACommand Client + REQ :: InvitationId -> ConnInfo -> ACommand Agent + ACPT :: InvitationId -> ConnInfo -> ACommand Client + CON :: ACommand Agent -- notification that connection is established + ICON :: ConnId -> ACommand Agent + SUB :: ACommand Client + SUBALL :: ACommand Client -- TODO should be moved to chat protocol - hack for subscribing to all + END :: ACommand Agent -- QST :: QueueDirection -> ACommand Client -- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent - SEND :: MsgBody -> ACommand Client SEND_ - SENT :: AgentMsgId -> ACommand Agent SENT_ + SEND :: MsgBody -> ACommand Client + SENT :: AgentMsgId -> ACommand Agent MSG :: { recipientMeta :: (AgentMsgId, UTCTime), brokerMeta :: (MsgId, UTCTime), @@ -334,46 +174,17 @@ data ACommand (p :: AParty) (c :: ACmdTag) where msgIntegrity :: MsgIntegrity, msgBody :: MsgBody } -> - ACommand Agent MSG_ + ACommand Agent -- ACK :: AgentMsgId -> ACommand Client -- RCVD :: AgentMsgId -> ACommand Agent - OFF :: ACommand Client MSG_ - DEL :: ACommand Client DEL_ - ADD :: Entity Conn_ -> ACommand Client ADD_ - REM :: Entity Conn_ -> ACommand Client REM_ - LS :: ACommand Client LS_ - MS :: [Entity Conn_] -> ACommand Agent MS_ - OK :: ACommand Agent OK_ - ERR :: AgentErrorType -> ACommand Agent ERR_ + OFF :: ACommand Client + DEL :: ACommand Client + OK :: ACommand Agent + ERR :: AgentErrorType -> ACommand Agent -deriving instance Eq (ACommand p c) +deriving instance Eq (ACommand p) -deriving instance Show (ACommand p c) - -instance TestEquality (ACommand p) where - testEquality NEW NEW = Just Refl - testEquality c@INV {} c'@INV {} = refl c c' - testEquality c@JOIN {} c'@JOIN {} = refl c c' - testEquality CON CON = Just Refl - testEquality c@ICON {} c'@ICON {} = refl c c' - testEquality SUB SUB = Just Refl - testEquality SUBALL SUBALL = Just Refl - testEquality END END = Just Refl - testEquality c@SEND {} c'@SEND {} = refl c c' - testEquality c@SENT {} c'@SENT {} = refl c c' - testEquality c@MSG {} c'@MSG {} = refl c c' - testEquality OFF OFF = Just Refl - testEquality DEL DEL = Just Refl - testEquality c@ADD {} c'@ADD {} = refl c c' - testEquality c@REM {} c'@REM {} = refl c c' - testEquality c@LS {} c'@LS {} = refl c c' - testEquality c@MS {} c'@MS {} = refl c c' - testEquality OK OK = Just Refl - testEquality c@ERR {} c'@ERR {} = refl c c' - testEquality _ _ = Nothing - -refl :: Eq a => a -> a -> Maybe (t :~: t) -refl x x' = if x == x' then Just Refl else Nothing +deriving instance Show (ACommand p) -- | SMP message formats. data SMPMessage @@ -405,13 +216,13 @@ data AMessage where -- | agent envelope for the client message A_MSG :: MsgBody -> AMessage -- | agent message for introduction - A_INTRO :: IntroEntity -> EntityInfo -> AMessage + A_INTRO :: IntroId -> ConnInfo -> AMessage -- | agent envelope for the sent invitation - A_INV :: Entity Conn_ -> SMPQueueInfo -> EntityInfo -> AMessage + A_INV :: IntroId -> SMPQueueInfo -> ConnInfo -> AMessage -- | agent envelope for the forwarded invitation - A_REQ :: Entity Conn_ -> SMPQueueInfo -> EntityInfo -> AMessage + A_REQ :: IntroId -> SMPQueueInfo -> ConnInfo -> AMessage -- | agent message for intro/group request - A_CON :: Entity Conn_ -> AMessage + A_CON :: IntroId -> AMessage deriving (Show) -- | Parse SMP message. @@ -462,11 +273,11 @@ agentMessageP = hello = HELLO <$> C.pubKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP a_msg = A_MSG <$> binaryBody - a_intro = A_INTRO <$> introEntityP <* A.space <*> binaryBody + a_intro = A_INTRO <$> A.takeTill (== ' ') <* A.space <*> binaryBody a_inv = invP A_INV a_req = invP A_REQ - a_con = A_CON <$> connEntityP - invP f = f <$> connEntityP <* A.space <*> smpQueueInfoP <* A.space <*> binaryBody + a_con = A_CON <$> A.takeTill wordEnd + invP f = f <$> A.takeTill (== ' ') <* A.space <*> smpQueueInfoP <* A.space <*> binaryBody binaryBody = do size :: Int <- A.decimal <* A.endOfLine A.take size <* A.endOfLine @@ -490,13 +301,13 @@ serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo A_MSG body -> "MSG " <> serializeMsg body <> "\n" - A_INTRO (IE entity) eInfo -> "INTRO " <> serializeIntro entity eInfo <> "\n" - A_INV conn qInfo eInfo -> "INV " <> serializeInv conn qInfo eInfo - A_REQ conn qInfo eInfo -> "REQ " <> serializeInv conn qInfo eInfo - A_CON conn -> "CON " <> serializeEntity conn + A_INTRO introId cInfo -> "INTRO " <> introId <> " " <> serializeMsg cInfo <> "\n" + A_INV introId qInfo cInfo -> "INV " <> serializeInv introId qInfo cInfo + A_REQ introId qInfo cInfo -> "REQ " <> serializeInv introId qInfo cInfo + A_CON introId -> "CON " <> introId where - serializeInv conn qInfo eInfo = - B.intercalate " " [serializeEntity conn, serializeSmpQueueInfo qInfo, serializeMsg eInfo] <> "\n" + serializeInv introId qInfo cInfo = + B.intercalate " " [introId, serializeSmpQueueInfo qInfo, serializeMsg cInfo] <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString @@ -522,6 +333,10 @@ instance IsString SMPServer where -- | SMP agent connection alias. type ConnId = ByteString +type IntroId = ByteString + +type InvitationId = ByteString + -- | Connection modes. data OnOff = On | Off deriving (Eq, Show, Read) @@ -583,8 +398,6 @@ data AgentErrorType CMD CommandErrorType | -- | connection errors CONN ConnectionErrorType - | -- | broadcast errors - BCAST BroadcastErrorType | -- | SMP protocol errors forwarded to agent clients SMP ErrorType | -- | SMP server errors @@ -599,14 +412,10 @@ data AgentErrorType data CommandErrorType = -- | command is prohibited in this context PROHIBITED - | -- | command is not supported by this entity - UNSUPPORTED | -- | command syntax is invalid SYNTAX - | -- | cannot parse entity - BAD_ENTITY | -- | entity ID is required with this command - NO_ENTITY + NO_CONN | -- | message size is not correct (no terminating space) SIZE | -- | message does not fit in SMP block @@ -623,14 +432,6 @@ data ConnectionErrorType SIMPLEX deriving (Eq, Generic, Read, Show, Exception) --- | Broadcast error -data BroadcastErrorType - = -- | broadcast ID is not in the database - B_NOT_FOUND - | -- | broadcast ID already exists - B_DUPLICATE - deriving (Eq, Generic, Read, Show, Exception) - -- | SMP server errors. data BrokerErrorType = -- | invalid server response (failed to parse) @@ -663,41 +464,10 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU -instance Arbitrary BroadcastErrorType where arbitrary = genericArbitraryU - instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU -anEntityP :: Parser AnEntity -anEntityP = - ($) - <$> ( "C:" $> AE . Conn - <|> "O:" $> AE . OpenConn - <|> "B:" $> AE . Broadcast - <|> "G:" $> AE . AGroup - ) - <*> A.takeTill wordEnd - -connEntityP :: Parser (Entity Conn_) -connEntityP = "C:" *> (Conn <$> A.takeTill wordEnd) - -introEntityP :: Parser IntroEntity -introEntityP = - ($) - <$> ( "C:" $> IE . Conn - <|> "O:" $> IE . OpenConn - <|> "G:" $> IE . AGroup - ) - <*> A.takeTill wordEnd - -serializeEntity :: Entity t -> ByteString -serializeEntity = \case - Conn s -> "C:" <> s - OpenConn s -> "O:" <> s - Broadcast s -> "B:" <> s - AGroup s -> "G:" <> s - -- | SMP agent command and response parser commandP :: Parser ACmd commandP = @@ -715,10 +485,6 @@ commandP = <|> "MSG " *> message <|> "OFF" $> ACmd SClient OFF <|> "DEL" $> ACmd SClient DEL - <|> "ADD " *> addCmd - <|> "REM " *> removeCmd - <|> "LS" $> ACmd SClient LS - <|> "MS " *> membersResp <|> "ERR " *> agentError <|> "ICON " *> iconMsg <|> "CON" $> ACmd SAgent CON @@ -731,10 +497,7 @@ commandP = acptCmd = ACmd SClient <$> introP ACPT sendCmd = ACmd SClient . SEND <$> A.takeByteString sentResp = ACmd SAgent . SENT <$> A.decimal - addCmd = ACmd SClient . ADD <$> connEntityP - removeCmd = ACmd SClient . REM <$> connEntityP - membersResp = ACmd SAgent . MS <$> (connEntityP `A.sepBy'` A.char ' ') - iconMsg = ACmd SAgent . ICON <$> introEntityP + iconMsg = ACmd SAgent . ICON <$> A.takeTill wordEnd message = do msgIntegrity <- msgIntegrityP <* A.space recipientMeta <- "R=" *> partyMeta A.decimal @@ -742,7 +505,7 @@ commandP = senderMeta <- "S=" *> partyMeta A.decimal msgBody <- A.takeByteString return $ ACmd SAgent MSG {recipientMeta, brokerMeta, senderMeta, msgIntegrity, msgBody} - introP f = f <$> introEntityP <* A.space <*> A.takeByteString + introP f = f <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On) partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space agentError = ACmd SAgent . ERR <$> agentErrorTypeP @@ -761,14 +524,14 @@ parseCommand :: ByteString -> Either AgentErrorType ACmd parseCommand = parse commandP $ CMD SYNTAX -- | Serialize SMP agent command. -serializeCommand :: ACommand p c -> ByteString +serializeCommand :: ACommand p -> ByteString serializeCommand = \case NEW -> "NEW" INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode - INTRO (IE entity) eInfo -> "INTRO " <> serializeIntro entity eInfo - REQ (IE entity) eInfo -> "REQ " <> serializeIntro entity eInfo - ACPT (IE entity) eInfo -> "ACPT " <> serializeIntro entity eInfo + INTRO connId cInfo -> "INTRO " <> connId <> " " <> serializeMsg cInfo + REQ invId cInfo -> "REQ " <> invId <> " " <> serializeMsg cInfo + ACPT invId cInfo -> "ACPT " <> invId <> " " <> serializeMsg cInfo SUB -> "SUB" SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all END -> "END" @@ -785,12 +548,8 @@ serializeCommand = \case ] OFF -> "OFF" DEL -> "DEL" - ADD c -> "ADD " <> serializeEntity c - REM c -> "REM " <> serializeEntity c - LS -> "LS" - MS cs -> "MS " <> B.intercalate " " (map serializeEntity cs) CON -> "CON" - ICON (IE entity) -> "ICON " <> serializeEntity entity + ICON introId -> "ICON " <> introId ERR e -> "ERR " <> serializeAgentError e OK -> "OK" where @@ -801,9 +560,6 @@ serializeCommand = \case showTs :: UTCTime -> ByteString showTs = B.pack . formatISO8601Millis -serializeIntro :: Entity t -> ByteString -> ByteString -serializeIntro entity eInfo = serializeEntity entity <> " " <> serializeMsg eInfo - -- | Serialize message integrity validation result. serializeMsgIntegrity :: MsgIntegrity -> ByteString serializeMsgIntegrity = \case @@ -820,7 +576,6 @@ serializeMsgIntegrity = \case agentErrorTypeP :: Parser AgentErrorType agentErrorTypeP = "SMP " *> (SMP <$> SMP.errorTypeP) - <|> "BCAST " *> (BCAST <$> bcastErrorP) <|> "BROKER RESPONSE " *> (BROKER . RESPONSE <$> SMP.errorTypeP) <|> "BROKER TRANSPORT " *> (BROKER . TRANSPORT <$> transportErrorP) <|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString) @@ -830,19 +585,10 @@ agentErrorTypeP = serializeAgentError :: AgentErrorType -> ByteString serializeAgentError = \case SMP e -> "SMP " <> SMP.serializeErrorType e - BCAST e -> "BCAST " <> serializeBcastError e BROKER (RESPONSE e) -> "BROKER RESPONSE " <> SMP.serializeErrorType e BROKER (TRANSPORT e) -> "BROKER TRANSPORT " <> serializeTransportError e e -> bshow e -bcastErrorP :: Parser BroadcastErrorType -bcastErrorP = "NOT_FOUND" $> B_NOT_FOUND <|> "DUPLICATE" $> B_DUPLICATE - -serializeBcastError :: BroadcastErrorType -> ByteString -serializeBcastError = \case - B_NOT_FOUND -> "NOT_FOUND" - B_DUPLICATE -> "DUPLICATE" - serializeMsg :: ByteString -> ByteString serializeMsg body = bshow (B.length body) <> "\n" <> body @@ -859,58 +605,45 @@ tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h -- | Send SMP agent protocol command (or response) to TCP connection. tPut :: (Transport c, MonadIO m) => c -> ATransmission p -> m () -tPut h (ATransmission corrId ent cmd) = - liftIO $ tPutRaw h (corrId, serializeEntity ent, serializeCommand cmd) +tPut h (corrId, connAlias, command) = + liftIO $ tPutRaw h (corrId, connAlias, serializeCommand command) -- | Receive client and agent transmissions from TCP connection. tGet :: forall c m p. (Transport c, MonadIO m) => SAParty p -> c -> m (ATransmissionOrError p) tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody where tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p) - tParseLoadBody (corrId, entityStr, command) = - case parseAll anEntityP entityStr of - Left _ -> pure $ ATransmissionOrError @_ @_ @ERR_ corrId (Conn "") $ Left $ CMD BAD_ENTITY - Right entity -> do - let cmd = parseCommand command >>= fromParty >>= hasEntityId entity - makeTransmission corrId entity <$> either (pure . Left) cmdWithMsgBody cmd + tParseLoadBody t@(corrId, connId, command) = do + let cmd = parseCommand command >>= fromParty >>= tConnId t + fullCmd <- either (return . Left) cmdWithMsgBody cmd + return (corrId, connId, fullCmd) - fromParty :: ACmd -> Either AgentErrorType (APartyCmd p) + fromParty :: ACmd -> Either AgentErrorType (ACommand p) fromParty (ACmd (p :: p1) cmd) = case testEquality party p of - Just Refl -> Right $ APartyCmd cmd + Just Refl -> Right cmd _ -> Left $ CMD PROHIBITED - hasEntityId :: AnEntity -> APartyCmd p -> Either AgentErrorType (APartyCmd p) - hasEntityId (AE entity) (APartyCmd cmd) = - APartyCmd <$> case cmd of - -- NEW, JOIN and ACPT have optional entity - NEW -> Right cmd - JOIN {} -> Right cmd - ACPT {} -> Right cmd - -- ERROR response does not always have entity - ERR _ -> Right cmd - -- other responses must have entity - _ - | B.null (entityId entity) -> Left $ CMD NO_ENTITY - | otherwise -> Right cmd + tConnId :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p) + tConnId (_, connId, _) cmd = case cmd of + -- NEW, JOIN and ACPT have optional connId + NEW -> Right cmd + JOIN {} -> Right cmd + ACPT {} -> Right cmd + -- ERROR response does not always have connId + ERR _ -> Right cmd + -- other responses must have connId + _ + | B.null connId -> Left $ CMD NO_CONN + | otherwise -> Right cmd - makeTransmission :: ACorrId -> AnEntity -> Either AgentErrorType (APartyCmd p) -> ATransmissionOrError p - makeTransmission corrId (AE entity) = \case - Left e -> err e - Right (APartyCmd cmd) -> case entityCommand entity cmd of - Just Dict -> ATransmissionOrError corrId entity $ Right cmd - _ -> err $ CMD UNSUPPORTED - where - err e = ATransmissionOrError @_ @_ @ERR_ corrId entity $ Left e - - cmdWithMsgBody :: APartyCmd p -> m (Either AgentErrorType (APartyCmd p)) - cmdWithMsgBody (APartyCmd cmd) = - APartyCmd <$$> case cmd of - SEND body -> SEND <$$> getMsgBody body - MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body - INTRO entity eInfo -> INTRO entity <$$> getMsgBody eInfo - REQ entity eInfo -> REQ entity <$$> getMsgBody eInfo - ACPT entity eInfo -> ACPT entity <$$> getMsgBody eInfo - _ -> pure $ Right cmd + cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p)) + cmdWithMsgBody = \case + SEND body -> SEND <$$> getMsgBody body + MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body + INTRO introId cInfo -> INTRO introId <$$> getMsgBody cInfo + REQ introId cInfo -> REQ introId <$$> getMsgBody cInfo + ACPT introId cInfo -> ACPT introId <$$> getMsgBody cInfo + cmd -> pure $ Right cmd -- TODO refactor with server getMsgBody :: MsgBody -> m (Either AgentErrorType MsgBody) diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index d1e71a3f6..7b4b03cbc 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -56,17 +56,10 @@ class Monad m => MonadAgentStore s m where getMsg :: s -> ConnId -> InternalId -> m Msg - -- Broadcasts - createBcast :: s -> TVar ChaChaDRG -> BroadcastId -> m BroadcastId - addBcastConn :: s -> BroadcastId -> ConnId -> m () - removeBcastConn :: s -> BroadcastId -> ConnId -> m () - deleteBcast :: s -> BroadcastId -> m () - getBcast :: s -> BroadcastId -> m [ConnId] - -- Introductions createIntro :: s -> TVar ChaChaDRG -> NewIntroduction -> m IntroId getIntro :: s -> IntroId -> m Introduction - addIntroInvitation :: s -> IntroId -> EntityInfo -> SMPQueueInfo -> m () + addIntroInvitation :: s -> IntroId -> ConnInfo -> SMPQueueInfo -> m () setIntroToStatus :: s -> IntroId -> IntroStatus -> m () setIntroReStatus :: s -> IntroId -> IntroStatus -> m () createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId @@ -334,12 +327,10 @@ introStatusT = \case "CON" -> Just IntroCon _ -> Nothing -type IntroId = ByteString - data NewInvitation = NewInvitation { viaConn :: ConnId, externalIntroId :: IntroId, - entityInfo :: EntityInfo, + connInfo :: ConnInfo, qInfo :: Maybe SMPQueueInfo } @@ -347,7 +338,7 @@ data Invitation = Invitation { invId :: InvitationId, viaConn :: ConnId, externalIntroId :: IntroId, - entityInfo :: EntityInfo, + connInfo :: ConnInfo, qInfo :: Maybe SMPQueueInfo, connId :: Maybe ConnId, status :: InvitationStatus @@ -370,8 +361,6 @@ invStatusT = \case "CON" -> Just InvCon _ -> Nothing -type InvitationId = ByteString - -- * Store errors -- | Agent store error. @@ -387,10 +376,6 @@ data StoreError | -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa. -- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error. SEBadConnType ConnType - | -- | Broadcast ID not found. - SEBcastNotFound - | -- | Broadcast ID already used. - SEBcastDuplicate | -- | Introduction ID not found. SEIntroNotFound | -- | Invitation ID not found. diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index d619026c4..e37096813 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -29,11 +29,9 @@ import Control.Monad (join, unless, when) import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) import Control.Monad.IO.Unlift (MonadUnliftIO) import Crypto.Random (ChaChaDRG, randomBytesGenerate) -import Data.Bifunctor (first) import Data.ByteString (ByteString) import Data.ByteString.Base64 (encode) import Data.Char (toLower) -import Data.Functor (($>)) import Data.List (find) import Data.Maybe (fromMaybe) import Data.Text (Text) @@ -114,11 +112,8 @@ connectSQLiteStore dbFilePath = do |] pure SQLiteStore {dbFilePath, dbConn, dbNew} -checkConstraint :: StoreError -> IO a -> IO (Either StoreError a) -checkConstraint err action = first (handleSQLError err) <$> E.try action - -checkConstraint' :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) -checkConstraint' err action = action `E.catch` (pure . Left . handleSQLError err) +checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) +checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e @@ -142,7 +137,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto createRcvConn SQLiteStore {dbConn} gVar cData q@RcvQueue {server} = -- TODO if schema has to be restarted, this function can be refactored -- to create connection first using createWithRandomId - liftIOEither . checkConstraint' SEConnDuplicate . withTransaction dbConn $ + liftIOEither . checkConstraint SEConnDuplicate . withTransaction dbConn $ getConnId_ dbConn gVar cData >>= traverse create where create :: ConnId -> IO ConnId @@ -156,7 +151,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto createSndConn SQLiteStore {dbConn} gVar cData q@SndQueue {server} = -- TODO if schema has to be restarted, this function can be refactored -- to create connection first using createWithRandomId - liftIOEither . checkConstraint' SEConnDuplicate . withTransaction dbConn $ + liftIOEither . checkConstraint SEConnDuplicate . withTransaction dbConn $ getConnId_ dbConn gVar cData >>= traverse create where create :: ConnId -> IO ConnId @@ -303,55 +298,6 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg getMsg _st _connAlias _id = throwError SENotImplemented - createBcast :: SQLiteStore -> TVar ChaChaDRG -> BroadcastId -> m BroadcastId - createBcast SQLiteStore {dbConn} gVar bcastId = liftIOEither $ case bcastId of - "" -> createWithRandomId gVar create - bId -> checkConstraint SEBcastDuplicate $ create bId $> bId - where - create bId = DB.execute dbConn "INSERT INTO broadcasts (broadcast_id) VALUES (?);" (Only bId) - - addBcastConn :: SQLiteStore -> BroadcastId -> ConnId -> m () - addBcastConn SQLiteStore {dbConn} bId connId = - liftIOEither . checkBroadcast dbConn bId $ - getConn_ dbConn connId >>= \case - Left _ -> pure $ Left SEConnNotFound - Right (SomeConn _ RcvConnection {}) -> pure . Left $ SEBadConnType CRcv - Right _ -> - checkConstraint SEConnDuplicate $ - DB.execute - dbConn - [sql| - INSERT INTO broadcast_connections - (broadcast_id, conn_alias) VALUES (?, ?); - |] - (bId, connId) - - removeBcastConn :: SQLiteStore -> BroadcastId -> ConnId -> m () - removeBcastConn SQLiteStore {dbConn} bId connId = - liftIOEither . checkBroadcast dbConn bId $ - bcastConnExists_ dbConn bId connId >>= \case - False -> pure $ Left SEConnNotFound - _ -> - Right - <$> DB.execute - dbConn - [sql| - DELETE FROM broadcast_connections - WHERE broadcast_id = ? AND conn_alias = ?; - |] - (bId, connId) - - deleteBcast :: SQLiteStore -> BroadcastId -> m () - deleteBcast SQLiteStore {dbConn} bId = - liftIOEither . checkBroadcast dbConn bId $ - Right <$> DB.execute dbConn "DELETE FROM broadcasts WHERE broadcast_id = ?;" (Only bId) - - getBcast :: SQLiteStore -> BroadcastId -> m [ConnId] - getBcast SQLiteStore {dbConn} bId = - liftIOEither . checkBroadcast dbConn bId $ - Right . map fromOnly - <$> DB.query dbConn "SELECT conn_alias FROM broadcast_connections WHERE broadcast_id = ?;" (Only bId) - createIntro :: SQLiteStore -> TVar ChaChaDRG -> NewIntroduction -> m IntroId createIntro SQLiteStore {dbConn} gVar NewIntroduction {toConn, reConn, reInfo} = liftIOEither . createWithRandomId gVar $ \introId -> @@ -380,7 +326,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto Right $ Introduction {introId, toConn, toInfo, toStatus, reConn, reInfo, reStatus, qInfo} intro _ = Left SEIntroNotFound - addIntroInvitation :: SQLiteStore -> IntroId -> EntityInfo -> SMPQueueInfo -> m () + addIntroInvitation :: SQLiteStore -> IntroId -> ConnInfo -> SMPQueueInfo -> m () addIntroInvitation SQLiteStore {dbConn} introId toInfo qInfo = liftIO $ DB.executeNamed @@ -423,7 +369,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto (reStatus, introId) createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId - createInvitation SQLiteStore {dbConn} gVar NewInvitation {viaConn, externalIntroId, entityInfo, qInfo} = + createInvitation SQLiteStore {dbConn} gVar NewInvitation {viaConn, externalIntroId, connInfo, qInfo} = liftIOEither . createWithRandomId gVar $ \invId -> DB.execute dbConn @@ -431,7 +377,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto INSERT INTO conn_invitations (inv_id, via_conn, external_intro_id, conn_info, queue_info) VALUES (?, ?, ?, ?, ?); |] - (invId, viaConn, externalIntroId, entityInfo, qInfo) + (invId, viaConn, externalIntroId, connInfo, qInfo) getInvitation :: SQLiteStore -> InvitationId -> m Invitation getInvitation SQLiteStore {dbConn} invId = @@ -446,8 +392,8 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] (Only invId) where - invitation [(viaConn, externalIntroId, entityInfo, qInfo, connId, status)] = - Right $ Invitation {invId, viaConn, externalIntroId, entityInfo, qInfo, connId, status} + invitation [(viaConn, externalIntroId, connInfo, qInfo, connId, status)] = + Right $ Invitation {invId, viaConn, externalIntroId, connInfo, qInfo, connId, status} invitation _ = Left SEInvitationNotFound addInvitationConn :: SQLiteStore -> InvitationId -> ConnId -> m () @@ -475,8 +421,8 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto (Only cId) >>= fmap join . traverse getViaConn . invitation where - invitation [(invId, viaConn, externalIntroId, entityInfo, qInfo, status)] = - Just $ Invitation {invId, viaConn, externalIntroId, entityInfo, qInfo, connId = Just cId, status} + invitation [(invId, viaConn, externalIntroId, connInfo, qInfo, status)] = + Just $ Invitation {invId, viaConn, externalIntroId, connInfo, qInfo, connId = Just cId, status} invitation _ = Nothing getViaConn :: Invitation -> IO (Maybe (Invitation, Connection 'CDuplex)) getViaConn inv@Invitation {viaConn} = fmap (inv,) . duplexConn <$> getConn_ dbConn viaConn @@ -926,34 +872,6 @@ updateHashSnd_ dbConn connId SndMsgData {..} = ":last_internal_snd_msg_id" := internalSndId ] --- * Broadcast helpers - -checkBroadcast :: DB.Connection -> BroadcastId -> IO (Either StoreError a) -> IO (Either StoreError a) -checkBroadcast dbConn bId action = - withTransaction dbConn $ do - ok <- bcastExists_ dbConn bId - if ok then action else pure $ Left SEBcastNotFound - -bcastExists_ :: DB.Connection -> BroadcastId -> IO Bool -bcastExists_ dbConn bId = not . null <$> queryBcast - where - queryBcast :: IO [Only BroadcastId] - queryBcast = DB.query dbConn "SELECT broadcast_id FROM broadcasts WHERE broadcast_id = ?;" (Only bId) - -bcastConnExists_ :: DB.Connection -> BroadcastId -> ConnId -> IO Bool -bcastConnExists_ dbConn bId connId = not . null <$> queryBcastConn - where - queryBcastConn :: IO [(BroadcastId, ConnId)] - queryBcastConn = - DB.query - dbConn - [sql| - SELECT broadcast_id, conn_alias - FROM broadcast_connections - WHERE broadcast_id = ? AND conn_alias = ?; - |] - (bId, connId) - -- create record with a random ID getConnId_ :: DB.Connection -> TVar ChaChaDRG -> ConnData -> IO (Either StoreError ConnId) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 57c7ad760..f60ca8eef 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -8,7 +8,6 @@ {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TupleSections #-} module AgentTests where @@ -18,7 +17,6 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store (InvitationId) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) import System.Timeout @@ -46,58 +44,41 @@ agentTests (ATransport t) = do smpAgentTest3_1_1 $ testSubscription t it "should send notifications to client when server disconnects" $ smpAgentServerTest $ testSubscrNotification t - describe "Broadcast" do - it "should create broadcast and send messages" $ - smpAgentTest3 $ testBroadcast t - it "should create broadcast and send messages (random IDs)" $ - smpAgentTest3 $ testBroadcastRandomIds t describe "Introduction" do it "should send and accept introduction" $ smpAgentTest3 $ testIntroduction t it "should send and accept introduction (random IDs)" $ smpAgentTest3 $ testIntroductionRandomIds t -type TestTransmission p = (ACorrId, ByteString, APartyCmd p) - -type TestTransmission' p c = (ACorrId, ByteString, ACommand p c) - -type TestTransmissionOrError p = (ACorrId, ByteString, Either AgentErrorType (APartyCmd p)) - -testTE :: ATransmissionOrError p -> TestTransmissionOrError p -testTE (ATransmissionOrError corrId entity cmdOrErr) = - (corrId,serializeEntity entity,) $ case cmdOrErr of - Right cmd -> Right $ APartyCmd cmd - Left e -> Left e - -- | receive message to handle `h` -(<#:) :: Transport c => c -> IO (TestTransmissionOrError 'Agent) -(<#:) h = testTE <$> tGet SAgent h +(<#:) :: Transport c => c -> IO (ATransmissionOrError 'Agent) +(<#:) = tGet SAgent -- | send transmission `t` to handle `h` and get response -(#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (TestTransmissionOrError 'Agent) -h #: t = tPutRaw h t >> (h <#:) +(#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent) +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 (TestTransmissionOrError 'Agent) -> TestTransmission' 'Agent c -> Expectation -action #> (corrId, cAlias, cmd) = action `shouldReturn` (corrId, cAlias, Right (APartyCmd cmd)) +(#>) :: IO (ATransmissionOrError 'Agent) -> ATransmission 'Agent -> Expectation +action #> (corrId, cAlias, cmd) = action `shouldReturn` (corrId, cAlias, 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 (TestTransmissionOrError 'Agent) -> (TestTransmission 'Agent -> Bool) -> Expectation +(=#>) :: IO (ATransmissionOrError 'Agent) -> (ATransmission 'Agent -> Bool) -> Expectation action =#> p = action >>= (`shouldSatisfy` p . correctTransmission) -correctTransmission :: TestTransmissionOrError p -> TestTransmission p +correctTransmission :: ATransmissionOrError a -> ATransmission a correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of Right cmd -> (corrId, cAlias, cmd) Left e -> error $ show e -- | receive message to handle `h` and validate that it is the expected one -(<#) :: Transport c => c -> TestTransmission' 'Agent c' -> Expectation -h <# (corrId, cAlias, cmd) = (h <#:) >>= (`shouldBe` (corrId, cAlias, Right (APartyCmd cmd))) +(<#) :: Transport c => c -> ATransmission 'Agent -> Expectation +h <# (corrId, cAlias, cmd) = (h <#:) >>= (`shouldBe` (corrId, cAlias, Right cmd)) -- | receive message to handle `h` and validate it using predicate `p` -(<#=) :: Transport c => c -> (TestTransmission 'Agent -> Bool) -> Expectation +(<#=) :: Transport c => c -> (ATransmission 'Agent -> Bool) -> Expectation h <#= p = (h <#:) >>= (`shouldSatisfy` p . correctTransmission) -- | test that nothing is delivered to handle `h` during 10ms @@ -109,42 +90,39 @@ h #:# err = tryGet `shouldReturn` () Just _ -> error err _ -> return () -pattern Sent :: AgentMsgId -> APartyCmd 'Agent -pattern Sent msgId <- APartyCmd (SENT msgId) +pattern Msg :: MsgBody -> ACommand 'Agent +pattern Msg msgBody <- MSG {msgBody, msgIntegrity = MsgOk} -pattern Msg :: MsgBody -> APartyCmd 'Agent -pattern Msg msgBody <- APartyCmd MSG {msgBody, msgIntegrity = MsgOk} +-- pattern Inv :: SMPQueueInfo -> Either AgentErrorType (ACommand 'Agent) +-- pattern Inv invitation <- Right (INV invitation) -pattern Inv :: SMPQueueInfo -> Either AgentErrorType (APartyCmd 'Agent) -pattern Inv invitation <- Right (APartyCmd (INV invitation)) - -pattern Req :: InvitationId -> EntityInfo -> Either AgentErrorType (APartyCmd 'Agent) -pattern Req invId eInfo <- Right (APartyCmd (REQ (IE (Conn invId)) eInfo)) +-- pattern Req :: InvitationId -> ConnInfo -> Either AgentErrorType (ACommand 'Agent) +-- pattern Req invId cInfo <- Right (REQ invId cInfo) testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do - ("1", "C:bob", Inv qInfo) <- alice #: ("1", "C:bob", "NEW") + ("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "C:alice", "JOIN " <> qInfo') #> ("", "C:alice", CON) - alice <# ("", "C:bob", CON) - alice #: ("2", "C:bob", "SEND :hello") #> ("2", "C:bob", SENT 1) - alice #: ("3", "C:bob", "SEND :how are you?") #> ("3", "C:bob", SENT 2) - bob <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False - bob <#= \case ("", "C:alice", Msg "how are you?") -> True; _ -> False - bob #: ("14", "C:alice", "SEND 9\nhello too") #> ("14", "C:alice", SENT 3) - alice <#= \case ("", "C:bob", Msg "hello too") -> True; _ -> False - bob #: ("15", "C:alice", "SEND 9\nmessage 1") #> ("15", "C:alice", SENT 4) - alice <#= \case ("", "C:bob", Msg "message 1") -> True; _ -> False - alice #: ("5", "C:bob", "OFF") #> ("5", "C:bob", OK) - bob #: ("17", "C:alice", "SEND 9\nmessage 3") #> ("17", "C:alice", ERR (SMP AUTH)) - alice #: ("6", "C:bob", "DEL") #> ("6", "C:bob", OK) + bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON) + alice <# ("", "bob", CON) + alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", SENT 1) + alice #: ("3", "bob", "SEND :how are you?") #> ("3", "bob", SENT 2) + bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False + bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False + bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", SENT 3) + alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False + bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", SENT 4) + alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False + alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) + bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", ERR (SMP AUTH)) + alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do - ("1", bobConn, Inv qInfo) <- alice #: ("1", "C:", "NEW") + ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("", aliceConn, Right (APartyCmd CON)) <- bob #: ("11", "C:", "JOIN " <> qInfo') + ("", aliceConn, Right CON) <- bob #: ("11", "", "JOIN " <> qInfo') alice <# ("", bobConn, CON) alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, SENT 1) alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, SENT 2) @@ -161,100 +139,26 @@ testDuplexConnRandomIds _ alice bob = do testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do - ("1", "C:bob", Inv qInfo) <- alice1 #: ("1", "C:bob", "NEW") + ("1", "bob", Right (INV qInfo)) <- alice1 #: ("1", "bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "C:alice", "JOIN " <> qInfo') #> ("", "C:alice", CON) - bob #: ("12", "C:alice", "SEND 5\nhello") #> ("12", "C:alice", SENT 1) - bob #: ("13", "C:alice", "SEND 11\nhello again") #> ("13", "C:alice", SENT 2) - alice1 <# ("", "C:bob", CON) - alice1 <#= \case ("", "C:bob", Msg "hello") -> True; _ -> False - alice1 <#= \case ("", "C:bob", Msg "hello again") -> True; _ -> False - alice2 #: ("21", "C:bob", "SUB") #> ("21", "C:bob", OK) - alice1 <# ("", "C:bob", END) - bob #: ("14", "C:alice", "SEND 2\nhi") #> ("14", "C:alice", SENT 3) - alice2 <#= \case ("", "C:bob", Msg "hi") -> True; _ -> False + bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON) + bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", SENT 1) + bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", SENT 2) + alice1 <# ("", "bob", CON) + alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False + alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False + alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK) + alice1 <# ("", "bob", END) + bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", SENT 3) + alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False alice1 #:# "nothing else should be delivered to alice1" testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () testSubscrNotification _ (server, _) client = do - client #: ("1", "C:conn1", "NEW") =#> \case ("1", "C:conn1", APartyCmd INV {}) -> True; _ -> False + client #: ("1", "conn1", "NEW") =#> \case ("1", "conn1", INV {}) -> True; _ -> False client #:# "nothing should be delivered to client before the server is killed" killThread server - client <# ("", "C:conn1", END) - -testBroadcast :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () -testBroadcast _ alice bob tom = do - -- establish connections - (alice, "alice") `connect` (bob, "bob") - (alice, "alice") `connect` (tom, "tom") - -- create and set up broadcast - alice #: ("1", "B:team", "NEW") #> ("1", "B:team", OK) - alice #: ("2", "B:team", "ADD C:bob") #> ("2", "B:team", OK) - alice #: ("3", "B:team", "ADD C:tom") #> ("3", "B:team", OK) - -- commands with errors - alice #: ("e1", "B:team", "NEW") #> ("e1", "B:team", ERR $ BCAST B_DUPLICATE) - alice #: ("e2", "B:group", "ADD C:bob") #> ("e2", "B:group", ERR $ BCAST B_NOT_FOUND) - alice #: ("e3", "B:team", "ADD C:unknown") #> ("e3", "B:team", ERR $ CONN NOT_FOUND) - alice #: ("e4", "B:team", "ADD C:bob") #> ("e4", "B:team", ERR $ CONN DUPLICATE) - -- send message - alice #: ("4", "B:team", "SEND 5\nhello") =#> \case ("4", c, Sent 1) -> c == "C:bob" || c == "C:tom"; _ -> False - alice <#= \case ("4", c, Sent 1) -> c == "C:bob" || c == "C:tom"; _ -> False - alice <# ("4", "B:team", SENT 0) - bob <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False - tom <#= \case ("", "C:alice", Msg "hello") -> True; _ -> False - -- remove one connection - alice #: ("5", "B:team", "REM C:tom") #> ("5", "B:team", OK) - alice #: ("6", "B:team", "SEND 11\nhello again") #> ("6", "C:bob", SENT 2) - alice <# ("6", "B:team", SENT 0) - bob <#= \case ("", "C:alice", Msg "hello again") -> True; _ -> False - tom #:# "nothing delivered to tom" - -- commands with errors - alice #: ("e5", "B:group", "REM C:bob") #> ("e5", "B:group", ERR $ BCAST B_NOT_FOUND) - alice #: ("e6", "B:team", "REM C:unknown") #> ("e6", "B:team", ERR $ CONN NOT_FOUND) - alice #: ("e7", "B:team", "REM C:tom") #> ("e7", "B:team", ERR $ CONN NOT_FOUND) - -- delete broadcast - alice #: ("7", "B:team", "DEL") #> ("7", "B:team", OK) - alice #: ("8", "B:team", "SEND 11\ntry sending") #> ("8", "B:team", ERR $ BCAST B_NOT_FOUND) - -- commands with errors - alice #: ("e8", "B:team", "DEL") #> ("e8", "B:team", ERR $ BCAST B_NOT_FOUND) - alice #: ("e9", "B:group", "DEL") #> ("e9", "B:group", ERR $ BCAST B_NOT_FOUND) - -testBroadcastRandomIds :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () -testBroadcastRandomIds _ alice bob tom = do - -- establish connections - (aliceB, bobA) <- alice `connect'` bob - (aliceT, tomA) <- alice `connect'` tom - -- create and set up broadcast - ("1", team, Right (APartyCmd OK)) <- alice #: ("1", "B:", "NEW") - alice #: ("2", team, "ADD " <> bobA) #> ("2", team, OK) - alice #: ("3", team, "ADD " <> tomA) #> ("3", team, OK) - -- commands with errors - alice #: ("e1", team, "NEW") #> ("e1", team, ERR $ BCAST B_DUPLICATE) - alice #: ("e2", "B:group", "ADD " <> bobA) #> ("e2", "B:group", ERR $ BCAST B_NOT_FOUND) - alice #: ("e3", team, "ADD C:unknown") #> ("e3", team, ERR $ CONN NOT_FOUND) - alice #: ("e4", team, "ADD " <> bobA) #> ("e4", team, ERR $ CONN DUPLICATE) - -- send message - alice #: ("4", team, "SEND 5\nhello") =#> \case ("4", c, Sent 1) -> c == bobA || c == tomA; _ -> False - alice <#= \case ("4", c, Sent 1) -> c == bobA || c == tomA; _ -> False - alice <# ("4", team, SENT 0) - bob <#= \case ("", c, Msg "hello") -> c == aliceB; _ -> False - tom <#= \case ("", c, Msg "hello") -> c == aliceT; _ -> False - -- remove one connection - alice #: ("5", team, "REM " <> tomA) #> ("5", team, OK) - alice #: ("6", team, "SEND 11\nhello again") #> ("6", bobA, SENT 2) - alice <# ("6", team, SENT 0) - bob <#= \case ("", c, Msg "hello again") -> c == aliceB; _ -> False - tom #:# "nothing delivered to tom" - -- commands with errors - alice #: ("e5", "B:group", "REM " <> bobA) #> ("e5", "B:group", ERR $ BCAST B_NOT_FOUND) - alice #: ("e6", team, "REM C:unknown") #> ("e6", team, ERR $ CONN NOT_FOUND) - alice #: ("e7", team, "REM " <> tomA) #> ("e7", team, ERR $ CONN NOT_FOUND) - -- delete broadcast - alice #: ("7", team, "DEL") #> ("7", team, OK) - alice #: ("8", team, "SEND 11\ntry sending") #> ("8", team, ERR $ BCAST B_NOT_FOUND) - -- commands with errors - alice #: ("e8", team, "DEL") #> ("e8", team, ERR $ BCAST B_NOT_FOUND) - alice #: ("e9", "B:group", "DEL") #> ("e9", "B:group", ERR $ BCAST B_NOT_FOUND) + client <# ("", "conn1", END) testIntroduction :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () testIntroduction _ alice bob tom = do @@ -262,21 +166,21 @@ testIntroduction _ alice bob tom = do (alice, "alice") `connect` (bob, "bob") (alice, "alice") `connect` (tom, "tom") -- send introduction of tom to bob - alice #: ("1", "C:bob", "INTRO C:tom 8\nmeet tom") #> ("1", "C:bob", OK) - ("", "C:alice", Req invId1 "meet tom") <- (bob <#:) - bob #: ("2", "C:tom_via_alice", "ACPT C:" <> invId1 <> " 7\nI'm bob") #> ("2", "C:tom_via_alice", OK) - ("", "C:alice", Req invId2 "I'm bob") <- (tom <#:) + alice #: ("1", "bob", "INTRO tom 8\nmeet tom") #> ("1", "bob", OK) + ("", "alice", Right (REQ invId1 "meet tom")) <- (bob <#:) + bob #: ("2", "tom_via_alice", "ACPT " <> invId1 <> " 7\nI'm bob") #> ("2", "tom_via_alice", OK) + ("", "alice", Right (REQ invId2 "I'm bob")) <- (tom <#:) -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter -- or this should be another command, not ACPT - tom #: ("3", "C:bob_via_alice", "ACPT C:" <> invId2 <> " 8\ntom here") #> ("3", "C:bob_via_alice", OK) - tom <# ("", "C:bob_via_alice", CON) - bob <# ("", "C:tom_via_alice", CON) - alice <# ("", "C:bob", ICON (IE (Conn "tom"))) + tom #: ("3", "bob_via_alice", "ACPT " <> invId2 <> " 8\ntom here") #> ("3", "bob_via_alice", OK) + tom <# ("", "bob_via_alice", CON) + bob <# ("", "tom_via_alice", CON) + alice <# ("", "bob", ICON "tom") -- they can message each other now - tom #: ("4", "C:bob_via_alice", "SEND :hello") #> ("4", "C:bob_via_alice", SENT 1) - bob <#= \case ("", "C:tom_via_alice", Msg "hello") -> True; _ -> False - bob #: ("5", "C:tom_via_alice", "SEND 9\nhello too") #> ("5", "C:tom_via_alice", SENT 2) - tom <#= \case ("", "C:bob_via_alice", Msg "hello too") -> True; _ -> False + tom #: ("4", "bob_via_alice", "SEND :hello") #> ("4", "bob_via_alice", SENT 1) + bob <#= \case ("", "tom_via_alice", Msg "hello") -> True; _ -> False + bob #: ("5", "tom_via_alice", "SEND 9\nhello too") #> ("5", "tom_via_alice", SENT 2) + tom <#= \case ("", "bob_via_alice", Msg "hello too") -> True; _ -> False testIntroductionRandomIds :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () testIntroductionRandomIds _ alice bob tom = do @@ -285,17 +189,17 @@ testIntroductionRandomIds _ alice bob tom = do (aliceT, tomA) <- alice `connect'` tom -- send introduction of tom to bob alice #: ("1", bobA, "INTRO " <> tomA <> " 8\nmeet tom") #> ("1", bobA, OK) - ("", aliceB', Req invId1 "meet tom") <- (bob <#:) + ("", aliceB', Right (REQ invId1 "meet tom")) <- (bob <#:) aliceB' `shouldBe` aliceB - ("2", tomB, Right (APartyCmd OK)) <- bob #: ("2", "C:", "ACPT C:" <> invId1 <> " 7\nI'm bob") - ("", aliceT', Req invId2 "I'm bob") <- (tom <#:) + ("2", tomB, Right OK) <- bob #: ("2", "C:", "ACPT " <> invId1 <> " 7\nI'm bob") + ("", aliceT', Right (REQ invId2 "I'm bob")) <- (tom <#:) aliceT' `shouldBe` aliceT -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter -- or this should be another command, not ACPT - ("3", bobT, Right (APartyCmd OK)) <- tom #: ("3", "C:", "ACPT C:" <> invId2 <> " 8\ntom here") + ("3", bobT, Right OK) <- tom #: ("3", "", "ACPT " <> invId2 <> " 8\ntom here") tom <# ("", bobT, CON) bob <# ("", tomB, CON) - alice <# ("", bobA, ICON . IE . Conn $ B.drop 2 tomA) + alice <# ("", bobA, ICON tomA) -- they can message each other now tom #: ("4", bobT, "SEND :hello") #> ("4", bobT, SENT 1) bob <#= \case ("", c, Msg "hello") -> c == tomB; _ -> False @@ -304,16 +208,16 @@ testIntroductionRandomIds _ alice bob tom = do connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do - ("c1", _, Inv qInfo) <- h1 #: ("c1", "C:" <> name2, "NEW") + ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") let qInfo' = serializeSmpQueueInfo qInfo - h2 #: ("c2", "C:" <> name1, "JOIN " <> qInfo') #> ("", "C:" <> name1, CON) - h1 <# ("", "C:" <> name2, CON) + h2 #: ("c2", name1, "JOIN " <> qInfo') #> ("", name1, CON) + h1 <# ("", name2, CON) connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) connect' h1 h2 = do - ("c1", conn2, Inv qInfo) <- h1 #: ("c1", "C:", "NEW") + ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("", conn1, Right (APartyCmd CON)) <- h2 #: ("c2", "C:", "JOIN " <> qInfo') + ("", conn1, Right CON) <- h2 #: ("c2", "", "JOIN " <> qInfo') h1 <# ("", conn2, CON) pure (conn1, conn2) @@ -322,25 +226,24 @@ samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSG syntaxTests :: forall c. Transport c => TProxy c -> Spec syntaxTests t = do - it "unknown command" $ ("1", "C:5678", "HELLO") >#> ("1", "C:5678", "ERR CMD SYNTAX") + it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX") describe "NEW" do describe "valid" do - -- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided) -- TODO: add tests with defined connection alias - xit "without parameters" $ ("211", "C:", "NEW") >#>= \case ("211", "C:", "INV" : _) -> True; _ -> False + it "without parameters" $ ("211", "", "NEW") >#>= \case ("211", _, "INV" : _) -> True; _ -> False describe "invalid" do -- TODO: add tests with defined connection alias - it "with parameters" $ ("222", "C:", "NEW hi") >#> ("222", "C:", "ERR CMD SYNTAX") + it "with parameters" $ ("222", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX") describe "JOIN" do describe "valid" do -- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided) -- TODO: add tests with defined connection alias it "using same server as in invitation" $ - ("311", "C:a", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "C:a", "ERR SMP AUTH") + ("311", "a", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "a", "ERR SMP AUTH") describe "invalid" do -- TODO: JOIN is not merged yet - to be added - it "no parameters" $ ("321", "C:", "JOIN") >#> ("321", "C:", "ERR CMD SYNTAX") + it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") where -- simple test for one command with the expected response (>#>) :: ARawTransmission -> ARawTransmission -> Expectation From d5f324cb5c200ad37206be936a78116cd67da93d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 13 Jun 2021 11:11:44 +0100 Subject: [PATCH 03/29] SMP agent: functional API (#159) * SMP agent: functional API (WIP) * functional API for SMP agent, tests * fix ICON message parameter * use stateTVar --- migrations/20210602_introductions.sql | 4 +- src/Simplex/Messaging/Agent.hs | 453 ++++++++++++++---------- src/Simplex/Messaging/Agent/Client.hs | 29 +- src/Simplex/Messaging/Agent/Protocol.hs | 5 +- src/Simplex/Messaging/Client.hs | 42 ++- tests/AgentTests.hs | 72 +++- tests/SMPAgentClient.hs | 14 +- 7 files changed, 379 insertions(+), 240 deletions(-) diff --git a/migrations/20210602_introductions.sql b/migrations/20210602_introductions.sql index d382b2961..36bb7539c 100644 --- a/migrations/20210602_introductions.sql +++ b/migrations/20210602_introductions.sql @@ -18,10 +18,10 @@ CREATE TABLE conn_invitations ( conn_id BLOB REFERENCES connections (conn_alias) -- created connection ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, - status TEXT DEFAULT '' -- '', 'ACPT', 'CON' + status TEXT NOT NULL DEFAULT '' -- '', 'ACPT', 'CON' ) WITHOUT ROWID; ALTER TABLE connections ADD via_inv BLOB REFERENCES conn_invitations (inv_id) ON DELETE RESTRICT; ALTER TABLE connections - ADD conn_level INTEGER DEFAULT 0; + ADD conn_level INTEGER NOT NULL DEFAULT 0; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index d2faf3774..84b5f69bf 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1,12 +1,15 @@ +{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} -- | -- Module : Simplex.Messaging.Agent @@ -21,10 +24,34 @@ -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md module Simplex.Messaging.Agent - ( runSMPAgent, + ( -- * SMP agent over TCP + runSMPAgent, runSMPAgentBlocking, + + -- * queue-based SMP agent + getAgentClient, + runAgentClient, + + -- * SMP agent functional API + AgentMonad, + AgentErrorMonad, getSMPAgentClient, - runSMPAgentClient, + createConnection, + joinConnection, + sendIntroduction, + acceptInvitation, + subscribeConnection, + sendMessage, + suspendConnection, + deleteConnection, + createConnection', + joinConnection', + sendIntroduction', + acceptInvitation', + subscribeConnection', + sendMessage', + suspendConnection', + deleteConnection', ) where @@ -34,8 +61,10 @@ import Control.Monad.Except import Control.Monad.IO.Unlift (MonadUnliftIO) import Control.Monad.Reader import Crypto.Random (MonadRandom) +import Data.Bifunctor (second) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Functor (($>)) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Text as T @@ -54,14 +83,14 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) import Simplex.Messaging.Util (bshow) import System.Random (randomR) -import UnliftIO.Async (race_) +import UnliftIO.Async (Async, async, race_) import qualified UnliftIO.Exception as E import UnliftIO.STM -- | Runs an SMP agent as a TCP service using passed configuration. -- -- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs -runSMPAgent :: (MonadFail m, MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m () +runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m () runSMPAgent t cfg = do started <- newEmptyTMVarIO runSMPAgentBlocking t started cfg @@ -70,23 +99,83 @@ runSMPAgent t cfg = do -- -- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True) -- and when it is disconnected from the TCP socket once the server thread is killed (False). -runSMPAgentBlocking :: (MonadFail m, MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m () +runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m () runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReaderT (smpAgent t) =<< newSMPAgentEnv cfg where - smpAgent :: forall c m'. (Transport c, MonadFail m', MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' () + smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' () smpAgent _ = runTransportServer started tcpPort $ \(h :: c) -> do liftIO $ putLn h "Welcome to SMP v0.3.2 agent" - c <- getSMPAgentClient + c <- getAgentClient logConnection c True - race_ (connectClient h c) (runSMPAgentClient c) - `E.finally` (closeSMPServerClients c >> logConnection c False) + race_ (connectClient h c) (runAgentClient c) + `E.finally` disconnectServers c --- | Creates an SMP agent instance that receives commands and sends responses via 'TBQueue's. -getSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient -getSMPAgentClient = do - n <- asks clientCounter - cfg <- asks config - atomically $ newAgentClient n cfg +-- | Creates an SMP agent client instance +getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m (Async (), AgentClient) +getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent + where + runAgent = do + c <- getAgentClient + st <- agentDB + action <- async $ subscriber c st `E.finally` disconnectServers c + pure (action, c) + +disconnectServers :: MonadUnliftIO m => AgentClient -> m () +disconnectServers c = closeSMPServerClients c >> logConnection c False + +-- | +type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) + +-- | Create SMP agent connection (NEW command) in Reader monad +createConnection' :: AgentMonad m => AgentClient -> m (ConnId, SMPQueueInfo) +createConnection' c = newConn c "" Nothing 0 + +-- | Create SMP agent connection (NEW command) +createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) +createConnection c = createConnection' c `runReaderT` agentEnv c + +-- | Join SMP agent connection (JOIN command) in Reader monad +joinConnection' :: AgentMonad m => AgentClient -> SMPQueueInfo -> m ConnId +joinConnection' c qInfo = joinConn c "" qInfo (ReplyMode On) Nothing 0 + +-- | Join SMP agent connection (JOIN command) +joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> m ConnId +joinConnection c qInfo = joinConnection' c qInfo `runReaderT` agentEnv c + +-- | Accept invitation (ACPT command) in Reader monad +acceptInvitation' :: AgentMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId +acceptInvitation' c = acceptInv c "" + +-- | Accept invitation (ACPT command) +acceptInvitation :: AgentErrorMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId +acceptInvitation c invId cInfo = acceptInvitation c invId cInfo `runReaderT` agentEnv c + +-- | Send introduction of the second connection the first (INTRO command) +sendIntroduction :: AgentErrorMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () +sendIntroduction c toConn reConn reInfo = sendIntroduction' c toConn reConn reInfo `runReaderT` agentEnv c + +-- | Subscribe to receive connection messages (SUB command) +subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () +subscribeConnection c connId = subscribeConnection' c connId `runReaderT` agentEnv c + +-- | Send message to the connection (SEND command) +sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId +sendMessage c connId msgBody = sendMessage' c connId msgBody `runReaderT` agentEnv c + +-- | Suspend SMP agent connection (OFF command) +suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () +suspendConnection c connId = suspendConnection' c connId `runReaderT` agentEnv c + +-- | Delete SMP agent connection (DEL command) +deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () +deleteConnection c connId = deleteConnection' c connId `runReaderT` agentEnv c + +-- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. +getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient +getAgentClient = do + store <- agentDB + env <- ask + atomically $ newAgentClient store env connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m () connectClient h c = race_ (send h c) (receive h c) @@ -97,30 +186,31 @@ logConnection c connected = in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"] -- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. -runSMPAgentClient :: (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () -runSMPAgentClient c = do +runAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () +runAgentClient c = do + st <- agentDB + race_ (subscriber c st) (client c) + +agentDB :: (MonadUnliftIO m, MonadReader Env m) => m SQLiteStore +agentDB = do db <- asks $ dbFile . config - s1 <- liftIO $ connectSQLiteStore db - s2 <- liftIO $ connectSQLiteStore db - race_ (subscriber c s1) (client c s2) + liftIO $ connectSQLiteStore db receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m () -receive h c@AgentClient {rcvQ, sndQ} = forever loop +receive h c@AgentClient {rcvQ, subQ} = forever $ do + (corrId, connId, cmdOrErr) <- tGet SClient h + case cmdOrErr of + Right cmd -> write rcvQ (corrId, connId, cmd) + Left e -> write subQ (corrId, connId, ERR e) where - loop :: m () - loop = do - (corrId, connId, cmdOrErr) <- tGet SClient h - case cmdOrErr of - Right cmd -> write rcvQ (corrId, connId, cmd) - Left e -> write sndQ (corrId, connId, ERR e) write :: TBQueue (ATransmission p) -> ATransmission p -> m () write q t = do logClient c "-->" t atomically $ writeTBQueue q t send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m () -send h c@AgentClient {sndQ} = forever $ do - t <- atomically $ readTBQueue sndQ +send h c@AgentClient {subQ} = forever $ do + t <- atomically $ readTBQueue subQ tPut h t logClient c "<--" t @@ -128,15 +218,13 @@ logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> logClient AgentClient {clientId} dir (corrId, connId, cmd) = do logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd] -client :: forall m. (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () -client c@AgentClient {rcvQ, sndQ} st = forever loop - where - loop :: m () - loop = do - t@(corrId, connId, _) <- atomically $ readTBQueue rcvQ - runExceptT (processCommand c st t) >>= \case - Left e -> atomically $ writeTBQueue sndQ (corrId, connId, ERR e) - Right _ -> pure () +client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () +client c@AgentClient {rcvQ, subQ} = forever $ do + (corrId, connId, cmd) <- atomically $ readTBQueue rcvQ + runExceptT (processCommand c (connId, cmd)) + >>= atomically . writeTBQueue subQ . \case + Left e -> (corrId, connId, ERR e) + Right (connId', resp) -> (corrId, connId', resp) withStore :: AgentMonad m => @@ -159,148 +247,148 @@ withStore action = do SEBadConnType CSnd -> CONN SIMPLEX e -> INTERNAL $ show e -processCommand :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ATransmission 'Client -> m () -processCommand c@AgentClient {sndQ} st (corrId, connId, cmd) = case cmd of - NEW -> createNewConnection Nothing 0 >>= uncurry respond - JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode Nothing 0 >> pure () -- >>= (`respond` OK) - INTRO reConnId reInfo -> makeIntroduction reConnId reInfo - ACPT invId connInfo -> acceptInvitation invId connInfo - SUB -> subscribeConnection connId - SUBALL -> subscribeAll - SEND msgBody -> sendMessage msgBody - OFF -> suspendConnection - DEL -> deleteConnection +-- | execute any SMP agent command +processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent) +processCommand c (connId, cmd) = case cmd of + NEW -> second INV <$> newConn c connId Nothing 0 + JOIN smpQueueInfo replyMode -> (,OK) <$> joinConn c connId smpQueueInfo replyMode Nothing 0 + INTRO reConnId reInfo -> sendIntroduction' c connId reConnId reInfo $> (connId, OK) + ACPT invId connInfo -> (,OK) <$> acceptInv c connId invId connInfo + SUB -> subscribeConnection' c connId $> (connId, OK) + SEND msgBody -> (connId,) . SENT . unId <$> sendMessage' c connId msgBody + OFF -> suspendConnection' c connId $> (connId, OK) + DEL -> deleteConnection' c connId $> (connId, OK) + +newConn :: AgentMonad m => AgentClient -> ConnId -> Maybe InvitationId -> Int -> m (ConnId, SMPQueueInfo) +newConn c connId viaInv connLevel = do + srv <- getSMPServer + (rq, qInfo) <- newReceiveQueue c srv + g <- asks idsDrg + let cData = ConnData {connId, viaInv, connLevel} + connId' <- withStore $ createRcvConn st g cData rq + addSubscription c rq connId' + pure (connId', qInfo) where - createNewConnection :: Maybe InvitationId -> Int -> m (ConnId, ACommand 'Agent) - createNewConnection viaInv connLevel = do - -- TODO create connection alias if not passed - -- make connId Maybe? - srv <- getSMPServer - (rq, qInfo) <- newReceiveQueue c srv - g <- asks idsDrg - let cData = ConnData {connId, viaInv, connLevel} - connId' <- withStore $ createRcvConn st g cData rq - addSubscription c rq connId' - pure (connId', INV qInfo) - - getSMPServer :: m SMPServer - getSMPServer = - asks (smpServers . config) >>= \case - srv :| [] -> pure srv - servers -> do - gen <- asks randomServer - i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1) - pure $ servers L.!! i - - joinConnection :: SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m ConnId - joinConnection qInfo (ReplyMode replyMode) viaInv connLevel = do - (sq, senderKey, verifyKey) <- newSendQueue qInfo - g <- asks idsDrg - let cData = ConnData {connId, viaInv, connLevel} - connId' <- withStore $ createSndConn st g cData sq - connectToSendQueue c st sq senderKey verifyKey - when (replyMode == On) $ createReplyQueue connId' sq - pure connId' - - makeIntroduction :: IntroId -> ConnInfo -> m () - makeIntroduction reConn reInfo = - withStore ((,) <$> getConn st connId <*> getConn st reConn) >>= \case - (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do - g <- asks idsDrg - introId <- withStore $ createIntro st g NewIntroduction {toConn = connId, reConn, reInfo} - sendControlMessage c sq $ A_INTRO introId reInfo - respond connId OK - _ -> throwError $ CONN SIMPLEX - - acceptInvitation :: InvitationId -> ConnInfo -> m () - acceptInvitation invId connInfo = - withStore (getInvitation st invId) >>= \case - Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> - withStore (getConn st viaConn) >>= \case - SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of - Nothing -> do - (connId', INV qInfo') <- createNewConnection (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId connId' - sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo - respond connId' OK - Just qInfo' -> do - connId' <- joinConnection qInfo' (ReplyMode On) (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId connId' - respond connId' OK - _ -> throwError $ CONN SIMPLEX - _ -> throwError $ CMD PROHIBITED - - subscribeConnection :: ConnId -> m () - subscribeConnection cId = - withStore (getConn st cId) >>= \case - SomeConn _ (DuplexConnection _ rq _) -> subscribe rq - SomeConn _ (RcvConnection _ rq) -> subscribe rq - _ -> throwError $ CONN SIMPLEX - where - subscribe rq = subscribeQueue c rq cId >> respond cId OK - - -- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct - subscribeAll :: m () - subscribeAll = withStore (getAllConnIds st) >>= mapM_ subscribeConnection - - sendMessage :: MsgBody -> m () - sendMessage msgBody = - withStore (getConn st connId) >>= \case - SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq - SomeConn _ (SndConnection _ sq) -> sendMsg sq - _ -> throwError $ CONN SIMPLEX - where - sendMsg :: SndQueue -> m () - sendMsg sq = do - internalTs <- liftIO getCurrentTime - (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st connId - let msgStr = - serializeSMPMessage - SMPMessage - { senderMsgId = unSndId internalSndId, - senderTimestamp = internalTs, - previousMsgHash, - agentMessage = A_MSG msgBody - } - msgHash = C.sha256Hash msgStr - withStore $ - createSndMsg st connId $ - SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} - sendAgentMessage c sq msgStr - atomically $ writeTBQueue sndQ (corrId, connId, SENT $ unId internalId) - - suspendConnection :: m () - suspendConnection = - withStore (getConn st connId) >>= \case - SomeConn _ (DuplexConnection _ rq _) -> suspend rq - SomeConn _ (RcvConnection _ rq) -> suspend rq - _ -> throwError $ CONN SIMPLEX - where - suspend rq = suspendQueue c rq >> respond connId OK - - deleteConnection :: m () - deleteConnection = - withStore (getConn st connId) >>= \case - SomeConn _ (DuplexConnection _ rq _) -> delete rq - SomeConn _ (RcvConnection _ rq) -> delete rq - _ -> delConn - where - delConn = withStore (deleteConn st connId) >> respond connId OK - delete rq = do - deleteQueue c rq - removeSubscription c connId - delConn + st = store c +joinConn :: forall m. AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m ConnId +joinConn c connId qInfo (ReplyMode replyMode) viaInv connLevel = do + (sq, senderKey, verifyKey) <- newSendQueue qInfo + g <- asks idsDrg + let cData = ConnData {connId, viaInv, connLevel} + connId' <- withStore $ createSndConn st g cData sq + connectToSendQueue c sq senderKey verifyKey + when (replyMode == On) $ createReplyQueue connId' sq + pure connId' + where + st = store c createReplyQueue :: ConnId -> SndQueue -> m () createReplyQueue cId sq = do srv <- getSMPServer - (rq, qInfo) <- newReceiveQueue c srv + (rq, qInfo') <- newReceiveQueue c srv addSubscription c rq cId withStore $ upgradeSndConnToDuplex st cId rq - sendControlMessage c sq $ REPLY qInfo + sendControlMessage c sq $ REPLY qInfo' - respond :: ConnId -> ACommand 'Agent -> m () - respond cId resp = atomically . writeTBQueue sndQ $ (corrId, cId, resp) +-- | Send introduction of the second connection the first (INTRO command) in Reader monad +sendIntroduction' :: AgentMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () +sendIntroduction' c toConn reConn reInfo = + withStore ((,) <$> getConn st toConn <*> getConn st reConn) >>= \case + (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do + g <- asks idsDrg + introId <- withStore $ createIntro st g NewIntroduction {toConn, reConn, reInfo} + sendControlMessage c sq $ A_INTRO introId reInfo + _ -> throwError $ CONN SIMPLEX + where + st = store c + +acceptInv :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId +acceptInv c connId invId connInfo = + withStore (getInvitation st invId) >>= \case + Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> + withStore (getConn st viaConn) >>= \case + SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of + Nothing -> do + (connId', qInfo') <- newConn c connId (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId connId' + sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo + pure connId' + Just qInfo' -> do + connId' <- joinConn c connId qInfo' (ReplyMode On) (Just invId) (connLevel + 1) + withStore $ addInvitationConn st invId connId' + pure connId' + _ -> throwError $ CONN SIMPLEX + _ -> throwError $ CMD PROHIBITED + where + st = store c + +-- | Subscribe to receive connection messages (SUB command) in Reader monad +subscribeConnection' :: AgentMonad m => AgentClient -> ConnId -> m () +subscribeConnection' c connId = + withStore (getConn (store c) connId) >>= \case + SomeConn _ (DuplexConnection _ rq _) -> subscribeQueue c rq connId + SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId + _ -> throwError $ CONN SIMPLEX + +-- | Send message to the connection (SEND command) in Reader monad +sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId +sendMessage' c connId msgBody = + withStore (getConn st connId) >>= \case + SomeConn _ (DuplexConnection _ _ sq) -> sendMsg_ sq + SomeConn _ (SndConnection _ sq) -> sendMsg_ sq + _ -> throwError $ CONN SIMPLEX + where + st = store c + sendMsg_ :: SndQueue -> m InternalId + sendMsg_ sq = do + internalTs <- liftIO getCurrentTime + (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st connId + let msgStr = + serializeSMPMessage + SMPMessage + { senderMsgId = unSndId internalSndId, + senderTimestamp = internalTs, + previousMsgHash, + agentMessage = A_MSG msgBody + } + msgHash = C.sha256Hash msgStr + withStore $ + createSndMsg st connId $ + SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} + sendAgentMessage c sq msgStr + pure internalId + +-- | Suspend SMP agent connection (OFF command) in Reader monad +suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m () +suspendConnection' c connId = + withStore (getConn (store c) connId) >>= \case + SomeConn _ (DuplexConnection _ rq _) -> suspendQueue c rq + SomeConn _ (RcvConnection _ rq) -> suspendQueue c rq + _ -> throwError $ CONN SIMPLEX + +-- | Delete SMP agent connection (DEL command) in Reader monad +deleteConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () +deleteConnection' c connId = + withStore (getConn st connId) >>= \case + SomeConn _ (DuplexConnection _ rq _) -> delete rq + SomeConn _ (RcvConnection _ rq) -> delete rq + _ -> withStore (deleteConn st connId) + where + st = store c + delete :: RcvQueue -> m () + delete rq = do + deleteQueue c rq + removeSubscription c connId + withStore (deleteConn st connId) + +getSMPServer :: AgentMonad m => m SMPServer +getSMPServer = + asks (smpServers . config) >>= \case + srv :| [] -> pure srv + servers -> do + gen <- asks randomServer + i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1) + pure $ servers L.!! i sendControlMessage :: AgentMonad m => AgentClient -> SndQueue -> AMessage -> m () sendControlMessage c sq agentMessage = do @@ -313,20 +401,19 @@ sendControlMessage c sq agentMessage = do agentMessage } -subscriber :: (MonadFail m, MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () +subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () subscriber c@AgentClient {msgQ} st = forever $ do - -- TODO this will only process messages and notifications t <- atomically $ readTBQueue msgQ runExceptT (processSMPTransmission c st t) >>= \case Left e -> liftIO $ print e Right _ -> return () processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> SMPServerTransmission -> m () -processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do +processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do withStore (getRcvConn st srv rId) >>= \case SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq - _ -> atomically $ writeTBQueue sndQ ("", "", ERR $ CONN NOT_FOUND) + _ -> atomically $ writeTBQueue subQ ("", "", ERR $ CONN NOT_FOUND) where processSMP :: SConnType c -> ConnData -> RcvQueue -> m () processSMP cType ConnData {connId} rq@RcvQueue {status} = @@ -358,7 +445,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do notify . ERR $ BROKER UNEXPECTED where notify :: ACommand 'Agent -> m () - notify msg = atomically $ writeTBQueue sndQ ("", connId, msg) + notify msg = atomically $ writeTBQueue subQ ("", connId, msg) prohibited :: m () prohibited = notify . ERR $ AGENT A_PROHIBITED @@ -369,7 +456,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do case status of New -> do -- TODO currently it automatically allows whoever sends the confirmation - -- Commands CONF and LET are not supported in v0.2 + -- TODO create invitation and send REQ withStore $ setRcvQueueStatus st rq Confirmed -- TODO update sender key in the store? secureQueue c rq senderKey @@ -395,7 +482,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do SCRcv -> do (sq, senderKey, verifyKey) <- newSendQueue qInfo withStore $ upgradeRcvConnToDuplex st connId sq - connectToSendQueue c st sq senderKey verifyKey + connectToSendQueue c sq senderKey verifyKey connected _ -> prohibited @@ -460,7 +547,7 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do | otherwise -> prohibited where sendConMsg :: ConnId -> ConnId -> m () - sendConMsg toConn reConn = atomically $ writeTBQueue sndQ ("", toConn, ICON reConn) + sendConMsg toConn reConn = atomically $ writeTBQueue subQ ("", toConn, ICON reConn) agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () agentClientMsg receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do @@ -502,12 +589,14 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do | internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash | otherwise = MsgError MsgDuplicate -- this case is not possible -connectToSendQueue :: AgentMonad m => AgentClient -> SQLiteStore -> SndQueue -> SenderPublicKey -> VerificationKey -> m () -connectToSendQueue c st sq senderKey verifyKey = do +connectToSendQueue :: AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> VerificationKey -> m () +connectToSendQueue c sq senderKey verifyKey = do sendConfirmation c sq senderKey withStore $ setSndQueueStatus st sq Confirmed sendHello c sq verifyKey withStore $ setSndQueueStatus st sq Active + where + st = store c newSendQueue :: (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index c61328edf..efb9d6699 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -31,6 +31,7 @@ module Simplex.Messaging.Agent.Client ) where +import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except import Control.Monad.IO.Unlift @@ -48,6 +49,7 @@ import Data.Time.Clock import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, SenderPublicKey) @@ -59,27 +61,30 @@ import UnliftIO.STM data AgentClient = AgentClient { rcvQ :: TBQueue (ATransmission 'Client), - sndQ :: TBQueue (ATransmission 'Agent), + subQ :: TBQueue (ATransmission 'Agent), msgQ :: TBQueue SMPServerTransmission, smpClients :: TVar (Map SMPServer SMPClient), subscrSrvrs :: TVar (Map SMPServer (Set ConnId)), subscrConns :: TVar (Map ConnId SMPServer), - clientId :: Int + clientId :: Int, + store :: SQLiteStore, + agentEnv :: Env } -newAgentClient :: TVar Int -> AgentConfig -> STM AgentClient -newAgentClient cc AgentConfig {tbqSize} = do - rcvQ <- newTBQueue tbqSize - sndQ <- newTBQueue tbqSize - msgQ <- newTBQueue tbqSize +newAgentClient :: SQLiteStore -> Env -> STM AgentClient +newAgentClient store agentEnv = do + let qSize = tbqSize $ config agentEnv + rcvQ <- newTBQueue qSize + subQ <- newTBQueue qSize + msgQ <- newTBQueue qSize smpClients <- newTVar M.empty subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty - clientId <- (+ 1) <$> readTVar cc - writeTVar cc clientId - return AgentClient {rcvQ, sndQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId} + clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, store, agentEnv} -type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m, MonadFail m) +-- | Agent monad with MonadReader Env and MonadError AgentErrorType +type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient getSMPServerClient c@AgentClient {smpClients, msgQ} srv = @@ -119,7 +124,7 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = deleteKeys ks m = S.foldr' M.delete m ks notifySub :: ConnId -> IO () - notifySub connId = atomically $ writeTBQueue (sndQ c) ("", connId, END) + notifySub connId = atomically $ writeTBQueue (subQ c) ("", connId, END) closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m () closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 4e0131e52..ab57e3d5e 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -161,7 +161,6 @@ data ACommand (p :: AParty) where CON :: ACommand Agent -- notification that connection is established ICON :: ConnId -> ACommand Agent SUB :: ACommand Client - SUBALL :: ACommand Client -- TODO should be moved to chat protocol - hack for subscribing to all END :: ACommand Agent -- QST :: QueueDirection -> ACommand Client -- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent @@ -478,7 +477,6 @@ commandP = <|> "REQ " *> reqCmd <|> "ACPT " *> acptCmd <|> "SUB" $> ACmd SClient SUB - <|> "SUBALL" $> ACmd SClient SUBALL -- TODO remove - hack for subscribing to all <|> "END" $> ACmd SAgent END <|> "SEND " *> sendCmd <|> "SENT " *> sentResp @@ -533,7 +531,6 @@ serializeCommand = \case REQ invId cInfo -> "REQ " <> invId <> " " <> serializeMsg cInfo ACPT invId cInfo -> "ACPT " <> invId <> " " <> serializeMsg cInfo SUB -> "SUB" - SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all END -> "END" SEND msgBody -> "SEND " <> serializeMsg msgBody SENT mId -> "SENT " <> bshow mId @@ -549,7 +546,7 @@ serializeCommand = \case OFF -> "OFF" DEL -> "DEL" CON -> "CON" - ICON introId -> "ICON " <> introId + ICON connId -> "ICON " <> connId ERR e -> "ERR " <> serializeAgentError e OK -> "OK" where diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 140b33f88..960f86ba4 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -53,9 +53,10 @@ import Control.Monad import Control.Monad.Trans.Class import Control.Monad.Trans.Except import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe +import Data.Maybe (fromMaybe) import Network.Socket (ServiceName) import Numeric.Natural import Simplex.Messaging.Agent.Protocol (SMPServer (..)) @@ -64,7 +65,7 @@ import Simplex.Messaging.Protocol import Simplex.Messaging.Transport (ATransport (..), TCP, THandle (..), TProxy, Transport (..), TransportError, clientHandshake, runTransportClient) import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Util (bshow, liftError, raceAny_) -import System.Timeout +import System.Timeout (timeout) -- | 'SMPClient' is a handle used to send commands to a specific SMP server. -- @@ -195,22 +196,27 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ dis process :: SMPClient -> IO () process SMPClient {rcvQ, sentCommands} = forever $ do (_, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ - cs <- readTVarIO sentCommands - case M.lookup corrId cs of - Nothing -> do - case respOrErr of - Right (Cmd SBroker cmd) -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd) - -- TODO send everything else to errQ and log in agent - _ -> return () - Just Request {queueId, responseVar} -> atomically $ do - modifyTVar sentCommands $ M.delete corrId - putTMVar responseVar $ - if queueId == qId - then case respOrErr of - Left e -> Left $ SMPResponseError e - Right (Cmd _ (ERR e)) -> Left $ SMPServerError e - Right r -> Right r - else Left SMPUnexpectedResponse + if B.null $ bs corrId + then sendMsg qId respOrErr + else do + cs <- readTVarIO sentCommands + case M.lookup corrId cs of + Nothing -> sendMsg qId respOrErr + Just Request {queueId, responseVar} -> atomically $ do + modifyTVar sentCommands $ M.delete corrId + putTMVar responseVar $ + if queueId == qId + then case respOrErr of + Left e -> Left $ SMPResponseError e + Right (Cmd _ (ERR e)) -> Left $ SMPServerError e + Right r -> Right r + else Left SMPUnexpectedResponse + + sendMsg :: QueueId -> Either ErrorType Cmd -> IO () + sendMsg qId = \case + Right (Cmd SBroker cmd) -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd) + -- TODO send everything else to errQ and log in agent + _ -> return () -- | Disconnects SMP client from the server and terminates client threads. closeSMPClient :: SMPClient -> IO () diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index f60ca8eef..c6c588f97 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -8,19 +8,28 @@ {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests where import AgentTests.SQLiteTests (storeTests) import Control.Concurrent +import Control.Monad.Except (catchError, runExceptT) +import Control.Monad.IO.Unlift import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient +import SMPClient (withSmpServer) +import Simplex.Messaging.Agent +import Simplex.Messaging.Agent.Client +import Simplex.Messaging.Agent.Env.SQLite (dbFile) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store (InternalId (..)) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) import System.Timeout import Test.Hspec +import UnliftIO.STM agentTests :: ATransport -> Spec agentTests (ATransport t) = do @@ -39,6 +48,8 @@ agentTests (ATransport t) = do smpAgentTest2_2_2 $ testDuplexConnection t it "should connect via 2 servers and 2 agents (random IDs)" $ smpAgentTest2_2_2 $ testDuplexConnRandomIds t + it "should connect via one server using SMP agent clients" $ + withSmpServer (ATransport t) testAgentClient describe "Connection subscriptions" do it "should connect via one server and one agent" $ smpAgentTest3_1_1 $ testSubscription t @@ -75,7 +86,7 @@ correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of -- | receive message to handle `h` and validate that it is the expected one (<#) :: Transport c => c -> ATransmission 'Agent -> Expectation -h <# (corrId, cAlias, cmd) = (h <#:) >>= (`shouldBe` (corrId, cAlias, Right cmd)) +h <# (corrId, cAlias, cmd) = (h <#:) `shouldReturn` (corrId, cAlias, Right cmd) -- | receive message to handle `h` and validate it using predicate `p` (<#=) :: Transport c => c -> (ATransmission 'Agent -> Bool) -> Expectation @@ -93,17 +104,12 @@ h #:# err = tryGet `shouldReturn` () pattern Msg :: MsgBody -> ACommand 'Agent pattern Msg msgBody <- MSG {msgBody, msgIntegrity = MsgOk} --- pattern Inv :: SMPQueueInfo -> Either AgentErrorType (ACommand 'Agent) --- pattern Inv invitation <- Right (INV invitation) - --- pattern Req :: InvitationId -> ConnInfo -> Either AgentErrorType (ACommand 'Agent) --- pattern Req invId cInfo <- Right (REQ invId cInfo) - testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do ("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON) + bob #: ("11", "alice", "JOIN " <> qInfo') #> ("11", "alice", OK) + bob <# ("", "alice", CON) alice <# ("", "bob", CON) alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", SENT 1) alice #: ("3", "bob", "SEND :how are you?") #> ("3", "bob", SENT 2) @@ -118,11 +124,48 @@ testDuplexConnection _ alice bob = do alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" +testAgentClient :: IO () +testAgentClient = do + (_, alice) <- getSMPAgentClient cfg + (_, bob) <- getSMPAgentClient cfg {dbFile = testDB2} + Right () <- runExceptT $ do + (bobId, qInfo) <- createConnection alice + aliceId <- joinConnection bob qInfo + get alice ##> ("", bobId, CON) + get bob ##> ("", aliceId, CON) + InternalId 1 <- sendMessage alice bobId "hello" + InternalId 2 <- sendMessage alice bobId "how are you?" + get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False + InternalId 3 <- sendMessage bob aliceId "hello too" + InternalId 4 <- sendMessage bob aliceId "message 1" + get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False + suspendConnection alice bobId + InternalId 0 <- sendMessage bob aliceId "message 2" `catchError` \(SMP AUTH) -> pure $ InternalId 0 + deleteConnection alice bobId + liftIO $ noMessages alice "nothing else should be delivered to alice" + pure () + where + (##>) :: MonadIO m => m (ATransmission 'Agent) -> ATransmission 'Agent -> m () + a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t) + (=##>) :: MonadIO m => m (ATransmission 'Agent) -> (ATransmission 'Agent -> Bool) -> m () + a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p) + noMessages :: AgentClient -> String -> Expectation + noMessages c err = tryGet `shouldReturn` () + where + tryGet = + 10000 `timeout` get c >>= \case + Just _ -> error err + _ -> return () + get c = atomically (readTBQueue $ subQ c) + testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("", aliceConn, Right CON) <- bob #: ("11", "", "JOIN " <> qInfo') + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> qInfo') + bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, SENT 1) alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, SENT 2) @@ -139,12 +182,9 @@ testDuplexConnRandomIds _ alice bob = do testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do - ("1", "bob", Right (INV qInfo)) <- alice1 #: ("1", "bob", "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON) + (alice1, "alice") `connect` (bob, "bob") bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", SENT 1) bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", SENT 2) - alice1 <# ("", "bob", CON) alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK) @@ -210,14 +250,16 @@ connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") let qInfo' = serializeSmpQueueInfo qInfo - h2 #: ("c2", name1, "JOIN " <> qInfo') #> ("", name1, CON) + h2 #: ("c2", name1, "JOIN " <> qInfo') #> ("c2", name1, OK) + h2 <# ("", name1, CON) h1 <# ("", name2, CON) connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) connect' h1 h2 = do ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("", conn1, Right CON) <- h2 #: ("c2", "", "JOIN " <> qInfo') + ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo') + h2 <# ("", conn1, CON) h1 <# ("", conn2, CON) pure (conn1, conn2) diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index fbbfd7ccb..918b276f0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -54,12 +54,12 @@ testDB3 = "tests/tmp/smp-agent3.test.protocol.db" smpAgentTest :: forall c. Transport c => TProxy c -> ARawTransmission -> IO ARawTransmission smpAgentTest _ cmd = runSmpAgentTest $ \(h :: c) -> tPutRaw h cmd >> tGetRaw h -runSmpAgentTest :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => (c -> m a) -> m a +runSmpAgentTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => (c -> m a) -> m a runSmpAgentTest test = withSmpServer t . withSmpAgent t $ testSMPAgentClient test where t = transport @c -runSmpAgentServerTest :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> c -> m a) -> m a +runSmpAgentServerTest :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => ((ThreadId, ThreadId) -> c -> m a) -> m a runSmpAgentServerTest test = withSmpServerThreadOn t testPort $ \server -> withSmpAgentThreadOn t (agentTestPort, testPort, testDB) $ @@ -70,7 +70,7 @@ runSmpAgentServerTest test = smpAgentServerTest :: Transport c => ((ThreadId, ThreadId) -> c -> IO ()) -> Expectation smpAgentServerTest test' = runSmpAgentServerTest test' `shouldReturn` () -runSmpAgentTestN :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a +runSmpAgentTestN :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => [(ServiceName, ServiceName, String)] -> ([c] -> m a) -> m a runSmpAgentTestN agents test = withSmpServer t $ run agents [] where run :: [(ServiceName, ServiceName, String)] -> [c] -> m a @@ -78,7 +78,7 @@ runSmpAgentTestN agents test = withSmpServer t $ run agents [] run (a@(p, _, _) : as) hs = withSmpAgentOn t a $ testSMPAgentClientOn p $ \h -> run as (h : hs) t = transport @c -runSmpAgentTestN_1 :: forall c m a. (Transport c, MonadFail m, MonadUnliftIO m, MonadRandom m) => Int -> ([c] -> m a) -> m a +runSmpAgentTestN_1 :: forall c m a. (Transport c, MonadUnliftIO m, MonadRandom m) => Int -> ([c] -> m a) -> m a runSmpAgentTestN_1 nClients test = withSmpServer t . withSmpAgent t $ run nClients [] where run :: Int -> [c] -> m a @@ -156,17 +156,17 @@ cfg = } } -withSmpAgentThreadOn :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a +withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a withSmpAgentThreadOn t (port', smpPort', db') = let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" (Just smpPort') testKeyHash]} in serverBracket (\started -> runSMPAgentBlocking t started cfg') (removeFile db') -withSmpAgentOn :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a +withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const -withSmpAgent :: (MonadFail m, MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a +withSmpAgent :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB) testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a From 09c6adeabc533537dcc039e2195123c6f7167ebe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:39:59 +0100 Subject: [PATCH 04/29] pass migration as parameter (to use in simplex-chat) (#161) * pass migration as parameter (to use in simplex-chat) * add connId parameter to create/joinConnection --- package.yaml | 1 + src/Simplex/Messaging/Agent.hs | 37 ++++++++++--------- src/Simplex/Messaging/Agent/Client.hs | 6 ++- src/Simplex/Messaging/Agent/Env/SQLite.hs | 3 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 13 ++++--- .../Agent/Store/SQLite/Migrations.hs | 3 +- tests/AgentTests.hs | 9 ++--- tests/AgentTests/SQLiteTests.hs | 3 +- 8 files changed, 42 insertions(+), 33 deletions(-) diff --git a/package.yaml b/package.yaml index a12a0333a..ce4f42a9c 100644 --- a/package.yaml +++ b/package.yaml @@ -30,6 +30,7 @@ dependencies: - base >= 4.7 && < 5 - base64-bytestring >= 1.0 && < 1.3 - bytestring == 0.10.* + - composition == 1.0.* - constraints == 0.12.* - containers == 0.6.* - cryptonite == 0.27.* diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 84b5f69bf..0e71cdc02 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -33,6 +33,7 @@ module Simplex.Messaging.Agent runAgentClient, -- * SMP agent functional API + AgentClient (..), AgentMonad, AgentErrorMonad, getSMPAgentClient, @@ -64,9 +65,11 @@ import Crypto.Random (MonadRandom) import Data.Bifunctor (second) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Composition ((.:), (.:.)) import Data.Functor (($>)) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L +import Data.Maybe (fromMaybe) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8) import Data.Time.Clock @@ -83,7 +86,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) import Simplex.Messaging.Util (bshow) import System.Random (randomR) -import UnliftIO.Async (Async, async, race_) +import UnliftIO.Async (async, race_) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -111,14 +114,14 @@ runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReader `E.finally` disconnectServers c -- | Creates an SMP agent client instance -getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m (Async (), AgentClient) +getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m AgentClient getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent where runAgent = do c <- getAgentClient st <- agentDB action <- async $ subscriber c st `E.finally` disconnectServers c - pure (action, c) + pure c {smpSubscriber = action} disconnectServers :: MonadUnliftIO m => AgentClient -> m () disconnectServers c = closeSMPServerClients c >> logConnection c False @@ -127,20 +130,20 @@ disconnectServers c = closeSMPServerClients c >> logConnection c False type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) -- | Create SMP agent connection (NEW command) in Reader monad -createConnection' :: AgentMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection' c = newConn c "" Nothing 0 +createConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> m (ConnId, SMPQueueInfo) +createConnection' c connId = newConn c (fromMaybe "" connId) Nothing 0 -- | Create SMP agent connection (NEW command) -createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection c = createConnection' c `runReaderT` agentEnv c +createConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> m (ConnId, SMPQueueInfo) +createConnection c = (`runReaderT` agentEnv c) . createConnection' c -- | Join SMP agent connection (JOIN command) in Reader monad -joinConnection' :: AgentMonad m => AgentClient -> SMPQueueInfo -> m ConnId -joinConnection' c qInfo = joinConn c "" qInfo (ReplyMode On) Nothing 0 +joinConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> m ConnId +joinConnection' c connId qInfo = joinConn c (fromMaybe "" connId) qInfo (ReplyMode On) Nothing 0 -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> m ConnId -joinConnection c qInfo = joinConnection' c qInfo `runReaderT` agentEnv c +joinConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> m ConnId +joinConnection c = (`runReaderT` agentEnv c) .: joinConnection' c -- | Accept invitation (ACPT command) in Reader monad acceptInvitation' :: AgentMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId @@ -148,27 +151,27 @@ acceptInvitation' c = acceptInv c "" -- | Accept invitation (ACPT command) acceptInvitation :: AgentErrorMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId -acceptInvitation c invId cInfo = acceptInvitation c invId cInfo `runReaderT` agentEnv c +acceptInvitation c = (`runReaderT` agentEnv c) .: acceptInvitation c -- | Send introduction of the second connection the first (INTRO command) sendIntroduction :: AgentErrorMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () -sendIntroduction c toConn reConn reInfo = sendIntroduction' c toConn reConn reInfo `runReaderT` agentEnv c +sendIntroduction c = (`runReaderT` agentEnv c) .:. sendIntroduction' c -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -subscribeConnection c connId = subscribeConnection' c connId `runReaderT` agentEnv c +subscribeConnection c = (`runReaderT` agentEnv c) . subscribeConnection' c -- | Send message to the connection (SEND command) sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId -sendMessage c connId msgBody = sendMessage' c connId msgBody `runReaderT` agentEnv c +sendMessage c = (`runReaderT` agentEnv c) .: sendMessage' c -- | Suspend SMP agent connection (OFF command) suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -suspendConnection c connId = suspendConnection' c connId `runReaderT` agentEnv c +suspendConnection c = (`runReaderT` agentEnv c) . suspendConnection' c -- | Delete SMP agent connection (DEL command) deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -deleteConnection c connId = deleteConnection' c connId `runReaderT` agentEnv c +deleteConnection c = (`runReaderT` agentEnv c) . deleteConnection' c -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index efb9d6699..abc35e47a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -31,6 +31,7 @@ module Simplex.Messaging.Agent.Client ) where +import Control.Concurrent.Async (Async) import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except @@ -68,7 +69,8 @@ data AgentClient = AgentClient subscrConns :: TVar (Map ConnId SMPServer), clientId :: Int, store :: SQLiteStore, - agentEnv :: Env + agentEnv :: Env, + smpSubscriber :: Async () } newAgentClient :: SQLiteStore -> Env -> STM AgentClient @@ -81,7 +83,7 @@ newAgentClient store agentEnv = do subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, store, agentEnv} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, store, agentEnv, smpSubscriber = undefined} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 13445643a..77e1e1d6f 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -12,6 +12,7 @@ import Network.Socket import Numeric.Natural import Simplex.Messaging.Agent.Protocol (SMPServer) import Simplex.Messaging.Agent.Store.SQLite +import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client import System.Random (StdGen, newStdGen) import UnliftIO.STM @@ -37,7 +38,7 @@ data Env = Env newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env newSMPAgentEnv config = do idsDrg <- newTVarIO =<< drgNew - _ <- liftIO $ createSQLiteStore $ dbFile config + _ <- liftIO $ createSQLiteStore (dbFile config) Migrations.app clientCounter <- newTVarIO 0 randomServer <- newTVarIO =<< liftIO newStdGen return Env {config, idsDrg, clientCounter, reservedMsgSize, randomServer} diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index e37096813..2de757207 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -47,6 +47,7 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Network.Socket (ServiceName) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store +import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration) import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Parsers (blobFieldParser) import qualified Simplex.Messaging.Protocol as SMP @@ -66,8 +67,8 @@ data SQLiteStore = SQLiteStore dbNew :: Bool } -createSQLiteStore :: FilePath -> IO SQLiteStore -createSQLiteStore dbFilePath = do +createSQLiteStore :: FilePath -> [Migration] -> IO SQLiteStore +createSQLiteStore dbFilePath migrations = do let dbDir = takeDirectory dbFilePath createDirectoryIfMissing False dbDir store <- connectSQLiteStore dbFilePath @@ -77,13 +78,13 @@ createSQLiteStore dbFilePath = do Just "THREADSAFE=0" -> confirmOrExit "SQLite compiled with non-threadsafe code." Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found" _ -> return () - migrateSchema store + migrateSchema store migrations pure store -migrateSchema :: SQLiteStore -> IO () -migrateSchema SQLiteStore {dbConn, dbFilePath, dbNew} = do +migrateSchema :: SQLiteStore -> [Migration] -> IO () +migrateSchema SQLiteStore {dbConn, dbFilePath, dbNew} migrations = do Migrations.initialize dbConn - Migrations.get dbConn Migrations.app >>= \case + Migrations.get dbConn migrations >>= \case Left e -> confirmOrExit $ "Database error: " <> e Right [] -> pure () Right ms -> do diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index b022ea5bf..4e6128493 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -7,7 +7,8 @@ {-# LANGUAGE TupleSections #-} module Simplex.Messaging.Agent.Store.SQLite.Migrations - ( app, + ( Migration (..), + app, initialize, get, run, diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index c6c588f97..34993c8e8 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -21,7 +21,6 @@ import qualified Data.ByteString.Char8 as B import SMPAgentClient import SMPClient (withSmpServer) import Simplex.Messaging.Agent -import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite (dbFile) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store (InternalId (..)) @@ -126,11 +125,11 @@ testDuplexConnection _ alice bob = do testAgentClient :: IO () testAgentClient = do - (_, alice) <- getSMPAgentClient cfg - (_, bob) <- getSMPAgentClient cfg {dbFile = testDB2} + alice <- getSMPAgentClient cfg + bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice - aliceId <- joinConnection bob qInfo + (bobId, qInfo) <- createConnection alice Nothing + aliceId <- joinConnection bob Nothing qInfo get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, CON) InternalId 1 <- sendMessage alice bobId "hello" diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 2f8383a8c..c3c1bcdd2 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -24,6 +24,7 @@ import SMPClient (testKeyHash) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite +import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C import System.Random (Random (randomIO)) import Test.Hspec @@ -49,7 +50,7 @@ createStore = do -- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous -- IO operations on multiple similarly named files; error seems to be environment specific r <- randomIO :: IO Word32 - createSQLiteStore $ testDB <> show r + createSQLiteStore (testDB <> show r) Migrations.app removeStore :: SQLiteStore -> IO () removeStore store = do From 7af727263532e42d88b15289773dc4dacf1af255 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:17:11 +0100 Subject: [PATCH 05/29] SQLiteStore connection pool (#162) * SQLiteStore connection pool * move SQLiteStore to agent env - single store instance is used for all clients --- apps/smp-agent/Main.hs | 1 + src/Simplex/Messaging/Agent.hs | 115 +++----- src/Simplex/Messaging/Agent/Client.hs | 8 +- src/Simplex/Messaging/Agent/Env/SQLite.hs | 10 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 306 ++++++++++---------- tests/AgentTests/SQLiteTests.hs | 23 +- tests/SMPAgentClient.hs | 1 + 7 files changed, 234 insertions(+), 230 deletions(-) diff --git a/apps/smp-agent/Main.hs b/apps/smp-agent/Main.hs index d2e7ae835..a632c63f9 100644 --- a/apps/smp-agent/Main.hs +++ b/apps/smp-agent/Main.hs @@ -20,6 +20,7 @@ cfg = connIdBytes = 12, tbqSize = 16, dbFile = "smp-agent.db", + dbPoolSize = 4, smpCfg = smpDefaultConfig } diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 0e71cdc02..5c455ba9a 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -78,7 +78,7 @@ import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, connectSQLiteStore) +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Client (SMPServerTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (MsgBody, SenderPublicKey) @@ -119,8 +119,7 @@ getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent where runAgent = do c <- getAgentClient - st <- agentDB - action <- async $ subscriber c st `E.finally` disconnectServers c + action <- async $ subscriber c `E.finally` disconnectServers c pure c {smpSubscriber = action} disconnectServers :: MonadUnliftIO m => AgentClient -> m () @@ -175,10 +174,7 @@ deleteConnection c = (`runReaderT` agentEnv c) . deleteConnection' c -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient -getAgentClient = do - store <- agentDB - env <- ask - atomically $ newAgentClient store env +getAgentClient = ask >>= atomically . newAgentClient connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m () connectClient h c = race_ (send h c) (receive h c) @@ -190,14 +186,7 @@ logConnection c connected = -- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. runAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () -runAgentClient c = do - st <- agentDB - race_ (subscriber c st) (client c) - -agentDB :: (MonadUnliftIO m, MonadReader Env m) => m SQLiteStore -agentDB = do - db <- asks $ dbFile . config - liftIO $ connectSQLiteStore db +runAgentClient c = race_ (subscriber c) (client c) receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m () receive h c@AgentClient {rcvQ, subQ} = forever $ do @@ -231,10 +220,11 @@ client c@AgentClient {rcvQ, subQ} = forever $ do withStore :: AgentMonad m => - (forall m'. (MonadUnliftIO m', MonadError StoreError m') => m' a) -> + (forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) -> m a withStore action = do - runExceptT (action `E.catch` handleInternal) >>= \case + st <- asks store' + runExceptT (action st `E.catch` handleInternal) >>= \case Right c -> return c Left e -> throwError $ storeError e where @@ -268,67 +258,60 @@ newConn c connId viaInv connLevel = do (rq, qInfo) <- newReceiveQueue c srv g <- asks idsDrg let cData = ConnData {connId, viaInv, connLevel} - connId' <- withStore $ createRcvConn st g cData rq + connId' <- withStore $ \st -> createRcvConn st g cData rq addSubscription c rq connId' pure (connId', qInfo) - where - st = store c joinConn :: forall m. AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m ConnId joinConn c connId qInfo (ReplyMode replyMode) viaInv connLevel = do (sq, senderKey, verifyKey) <- newSendQueue qInfo g <- asks idsDrg let cData = ConnData {connId, viaInv, connLevel} - connId' <- withStore $ createSndConn st g cData sq + connId' <- withStore $ \st -> createSndConn st g cData sq connectToSendQueue c sq senderKey verifyKey when (replyMode == On) $ createReplyQueue connId' sq pure connId' where - st = store c createReplyQueue :: ConnId -> SndQueue -> m () createReplyQueue cId sq = do srv <- getSMPServer (rq, qInfo') <- newReceiveQueue c srv addSubscription c rq cId - withStore $ upgradeSndConnToDuplex st cId rq + withStore $ \st -> upgradeSndConnToDuplex st cId rq sendControlMessage c sq $ REPLY qInfo' -- | Send introduction of the second connection the first (INTRO command) in Reader monad sendIntroduction' :: AgentMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () sendIntroduction' c toConn reConn reInfo = - withStore ((,) <$> getConn st toConn <*> getConn st reConn) >>= \case + withStore (\st -> (,) <$> getConn st toConn <*> getConn st reConn) >>= \case (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do g <- asks idsDrg - introId <- withStore $ createIntro st g NewIntroduction {toConn, reConn, reInfo} + introId <- withStore $ \st -> createIntro st g NewIntroduction {toConn, reConn, reInfo} sendControlMessage c sq $ A_INTRO introId reInfo _ -> throwError $ CONN SIMPLEX - where - st = store c acceptInv :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId acceptInv c connId invId connInfo = - withStore (getInvitation st invId) >>= \case + withStore (`getInvitation` invId) >>= \case Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> - withStore (getConn st viaConn) >>= \case + withStore (`getConn` viaConn) >>= \case SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of Nothing -> do (connId', qInfo') <- newConn c connId (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId connId' + withStore $ \st -> addInvitationConn st invId connId' sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo pure connId' Just qInfo' -> do connId' <- joinConn c connId qInfo' (ReplyMode On) (Just invId) (connLevel + 1) - withStore $ addInvitationConn st invId connId' + withStore $ \st -> addInvitationConn st invId connId' pure connId' _ -> throwError $ CONN SIMPLEX _ -> throwError $ CMD PROHIBITED - where - st = store c -- | Subscribe to receive connection messages (SUB command) in Reader monad subscribeConnection' :: AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = - withStore (getConn (store c) connId) >>= \case + withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> subscribeQueue c rq connId SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId _ -> throwError $ CONN SIMPLEX @@ -336,16 +319,15 @@ subscribeConnection' c connId = -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId sendMessage' c connId msgBody = - withStore (getConn st connId) >>= \case + withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ _ sq) -> sendMsg_ sq SomeConn _ (SndConnection _ sq) -> sendMsg_ sq _ -> throwError $ CONN SIMPLEX where - st = store c sendMsg_ :: SndQueue -> m InternalId sendMsg_ sq = do internalTs <- liftIO getCurrentTime - (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st connId + (internalId, internalSndId, previousMsgHash) <- withStore (`updateSndIds` connId) let msgStr = serializeSMPMessage SMPMessage @@ -355,7 +337,7 @@ sendMessage' c connId msgBody = agentMessage = A_MSG msgBody } msgHash = C.sha256Hash msgStr - withStore $ + withStore $ \st -> createSndMsg st connId $ SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} sendAgentMessage c sq msgStr @@ -364,7 +346,7 @@ sendMessage' c connId msgBody = -- | Suspend SMP agent connection (OFF command) in Reader monad suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m () suspendConnection' c connId = - withStore (getConn (store c) connId) >>= \case + withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> suspendQueue c rq SomeConn _ (RcvConnection _ rq) -> suspendQueue c rq _ -> throwError $ CONN SIMPLEX @@ -372,17 +354,16 @@ suspendConnection' c connId = -- | Delete SMP agent connection (DEL command) in Reader monad deleteConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () deleteConnection' c connId = - withStore (getConn st connId) >>= \case + withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> delete rq SomeConn _ (RcvConnection _ rq) -> delete rq - _ -> withStore (deleteConn st connId) + _ -> withStore (`deleteConn` connId) where - st = store c delete :: RcvQueue -> m () delete rq = do deleteQueue c rq removeSubscription c connId - withStore (deleteConn st connId) + withStore (`deleteConn` connId) getSMPServer :: AgentMonad m => m SMPServer getSMPServer = @@ -404,16 +385,16 @@ sendControlMessage c sq agentMessage = do agentMessage } -subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m () -subscriber c@AgentClient {msgQ} st = forever $ do +subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () +subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ - runExceptT (processSMPTransmission c st t) >>= \case + runExceptT (processSMPTransmission c t) >>= \case Left e -> liftIO $ print e Right _ -> return () -processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> SMPServerTransmission -> m () -processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do - withStore (getRcvConn st srv rId) >>= \case +processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SMPServerTransmission -> m () +processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do + withStore (\st -> getRcvConn st srv rId) >>= \case SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq _ -> atomically $ writeTBQueue subQ ("", "", ERR $ CONN NOT_FOUND) @@ -460,10 +441,10 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do New -> do -- TODO currently it automatically allows whoever sends the confirmation -- TODO create invitation and send REQ - withStore $ setRcvQueueStatus st rq Confirmed + withStore $ \st -> setRcvQueueStatus st rq Confirmed -- TODO update sender key in the store? secureQueue c rq senderKey - withStore $ setRcvQueueStatus st rq Secured + withStore $ \st -> setRcvQueueStatus st rq Secured _ -> prohibited helloMsg :: SenderPublicKey -> ByteString -> m () @@ -473,7 +454,7 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do Active -> prohibited _ -> do void $ verifyMessage (Just verifyKey) msgBody - withStore $ setRcvQueueActive st rq verifyKey + withStore $ \st -> setRcvQueueActive st rq verifyKey case cType of SCDuplex -> connected _ -> pure () @@ -484,16 +465,16 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do case cType of SCRcv -> do (sq, senderKey, verifyKey) <- newSendQueue qInfo - withStore $ upgradeRcvConnToDuplex st connId sq + withStore $ \st -> upgradeRcvConnToDuplex st connId sq connectToSendQueue c sq senderKey verifyKey connected _ -> prohibited connected :: m () connected = do - withStore (getConnInvitation st connId) >>= \case + withStore (`getConnInvitation` connId) >>= \case Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do - withStore $ setInvitationStatus st invId InvCon + withStore $ \st -> setInvitationStatus st invId InvCon sendControlMessage c sq $ A_CON externalIntroId _ -> pure () notify CON @@ -510,14 +491,14 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do logServer "<--" c srv rId "MSG " case cType of SCDuplex -> - withStore (getIntro st introId) >>= \case + withStore (`getIntro` introId) >>= \case Introduction {toConn, toStatus = IntroNew, reConn, reStatus = IntroNew} | toConn /= connId -> prohibited | otherwise -> - withStore (addIntroInvitation st introId toInfo qInfo >> getConn st reConn) >>= \case + withStore (\st -> addIntroInvitation st introId toInfo qInfo >> getConn st reConn) >>= \case SomeConn _ (DuplexConnection _ _ sq) -> do sendControlMessage c sq $ A_REQ introId qInfo toInfo - withStore $ setIntroReStatus st introId IntroInv + withStore $ \st -> setIntroReStatus st introId IntroInv _ -> prohibited _ -> prohibited _ -> prohibited @@ -533,19 +514,19 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do createInv externalIntroId qInfo connInfo = do g <- asks idsDrg let newInv = NewInvitation {viaConn = connId, externalIntroId, connInfo, qInfo} - invId <- withStore $ createInvitation st g newInv + invId <- withStore $ \st -> createInvitation st g newInv notify $ REQ invId connInfo conMsg :: IntroId -> m () conMsg introId = do logServer "<--" c srv rId "MSG " - withStore (getIntro st introId) >>= \case + withStore (`getIntro` introId) >>= \case Introduction {toConn, toStatus, reConn, reStatus} | toConn == connId && toStatus == IntroInv -> do - withStore $ setIntroToStatus st introId IntroCon + withStore $ \st -> setIntroToStatus st introId IntroCon when (reStatus == IntroCon) $ sendConMsg toConn reConn | reConn == connId && reStatus == IntroInv -> do - withStore $ setIntroReStatus st introId IntroCon + withStore $ \st -> setIntroReStatus st introId IntroCon when (toStatus == IntroCon) $ sendConMsg toConn reConn | otherwise -> prohibited where @@ -558,9 +539,9 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do case status of Active -> do internalTs <- liftIO getCurrentTime - (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st connId + (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId) let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash receivedPrevMsgHash - withStore $ + withStore $ \st -> createRcvMsg st connId $ RcvMsgData { internalId, @@ -595,11 +576,9 @@ processSMPTransmission c@AgentClient {subQ} st (srv, rId, cmd) = do connectToSendQueue :: AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> VerificationKey -> m () connectToSendQueue c sq senderKey verifyKey = do sendConfirmation c sq senderKey - withStore $ setSndQueueStatus st sq Confirmed + withStore $ \st -> setSndQueueStatus st sq Confirmed sendHello c sq verifyKey - withStore $ setSndQueueStatus st sq Active - where - st = store c + withStore $ \st -> setSndQueueStatus st sq Active newSendQueue :: (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index abc35e47a..7af54fb37 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -50,7 +50,6 @@ import Data.Time.Clock import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, SenderPublicKey) @@ -68,13 +67,12 @@ data AgentClient = AgentClient subscrSrvrs :: TVar (Map SMPServer (Set ConnId)), subscrConns :: TVar (Map ConnId SMPServer), clientId :: Int, - store :: SQLiteStore, agentEnv :: Env, smpSubscriber :: Async () } -newAgentClient :: SQLiteStore -> Env -> STM AgentClient -newAgentClient store agentEnv = do +newAgentClient :: Env -> STM AgentClient +newAgentClient agentEnv = do let qSize = tbqSize $ config agentEnv rcvQ <- newTBQueue qSize subQ <- newTBQueue qSize @@ -83,7 +81,7 @@ newAgentClient store agentEnv = do subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, store, agentEnv, smpSubscriber = undefined} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, agentEnv, smpSubscriber = undefined} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 77e1e1d6f..6d1fa30ba 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -24,11 +24,13 @@ data AgentConfig = AgentConfig connIdBytes :: Int, tbqSize :: Natural, dbFile :: FilePath, + dbPoolSize :: Int, smpCfg :: SMPClientConfig } data Env = Env { config :: AgentConfig, + store' :: SQLiteStore, idsDrg :: TVar ChaChaDRG, clientCounter :: TVar Int, reservedMsgSize :: Int, @@ -36,15 +38,15 @@ data Env = Env } newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env -newSMPAgentEnv config = do +newSMPAgentEnv cfg = do idsDrg <- newTVarIO =<< drgNew - _ <- liftIO $ createSQLiteStore (dbFile config) Migrations.app + store' <- liftIO $ createSQLiteStore (dbFile cfg) (dbPoolSize cfg) Migrations.app clientCounter <- newTVarIO 0 randomServer <- newTVarIO =<< liftIO newStdGen - return Env {config, idsDrg, clientCounter, reservedMsgSize, randomServer} + return Env {config = cfg, store', idsDrg, clientCounter, reservedMsgSize, randomServer} where -- 1st rsaKeySize is used by the RSA signature in each command, -- 2nd - by encrypted message body header -- 3rd - by message signature -- smpCommandSize - is the estimated max size for SMP command, queueId, corrId - reservedMsgSize = 3 * rsaKeySize config + smpCommandSize (smpCfg config) + reservedMsgSize = 3 * rsaKeySize cfg + smpCommandSize (smpCfg cfg) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 2de757207..ce46d964c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -20,12 +20,14 @@ module Simplex.Messaging.Agent.Store.SQLite ( SQLiteStore (..), createSQLiteStore, connectSQLiteStore, + withConnection, ) where import Control.Concurrent (threadDelay) -import Control.Concurrent.STM (TVar, atomically, stateTVar) -import Control.Monad (join, unless, when) +import Control.Concurrent.STM +import Control.Exception (bracket) +import Control.Monad (join, replicateM_, unless, when) import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) import Control.Monad.IO.Unlift (MonadUnliftIO) import Crypto.Random (ChaChaDRG, randomBytesGenerate) @@ -63,35 +65,40 @@ import qualified UnliftIO.Exception as E data SQLiteStore = SQLiteStore { dbFilePath :: FilePath, - dbConn :: DB.Connection, + dbConnPool :: TBQueue DB.Connection, dbNew :: Bool } -createSQLiteStore :: FilePath -> [Migration] -> IO SQLiteStore -createSQLiteStore dbFilePath migrations = do +createSQLiteStore :: FilePath -> Int -> [Migration] -> IO SQLiteStore +createSQLiteStore dbFilePath poolSize migrations = do let dbDir = takeDirectory dbFilePath createDirectoryIfMissing False dbDir - store <- connectSQLiteStore dbFilePath - compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[Text]] + st <- connectSQLiteStore dbFilePath poolSize + checkThreadsafe st + migrateSchema st migrations + pure st + +checkThreadsafe :: SQLiteStore -> IO () +checkThreadsafe st = withConnection st $ \db -> do + compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[Text]] let threadsafeOption = find (T.isPrefixOf "THREADSAFE=") (concat compileOptions) case threadsafeOption of Just "THREADSAFE=0" -> confirmOrExit "SQLite compiled with non-threadsafe code." Nothing -> putStrLn "Warning: SQLite THREADSAFE compile option not found" _ -> return () - migrateSchema store migrations - pure store migrateSchema :: SQLiteStore -> [Migration] -> IO () -migrateSchema SQLiteStore {dbConn, dbFilePath, dbNew} migrations = do - Migrations.initialize dbConn - Migrations.get dbConn migrations >>= \case +migrateSchema st migrations = withConnection st $ \db -> do + Migrations.initialize db + Migrations.get db migrations >>= \case Left e -> confirmOrExit $ "Database error: " <> e Right [] -> pure () Right ms -> do - unless dbNew $ do + unless (dbNew st) $ do confirmOrExit "The app has a newer version than the database - it will be backed up and upgraded." - copyFile dbFilePath $ dbFilePath <> ".bak" - Migrations.run dbConn ms + let f = dbFilePath st + copyFile f (f <> ".bak") + Migrations.run db ms confirmOrExit :: String -> IO () confirmOrExit s = do @@ -101,17 +108,19 @@ confirmOrExit s = do ok <- getLine when (map toLower ok /= "y") exitFailure -connectSQLiteStore :: FilePath -> IO SQLiteStore -connectSQLiteStore dbFilePath = do +connectSQLiteStore :: FilePath -> Int -> IO SQLiteStore +connectSQLiteStore dbFilePath poolSize = do dbNew <- not <$> doesFileExist dbFilePath - dbConn <- DB.open dbFilePath - DB.execute_ - dbConn - [sql| - PRAGMA foreign_keys = ON; - PRAGMA journal_mode = WAL; - |] - pure SQLiteStore {dbFilePath, dbConn, dbNew} + dbConnPool <- newTBQueueIO $ toEnum poolSize + replicateM_ poolSize $ + connectDB dbFilePath >>= atomically . writeTBQueue dbConnPool + pure SQLiteStore {dbFilePath, dbConnPool, dbNew} + +connectDB :: FilePath -> IO DB.Connection +connectDB path = do + dbConn <- DB.open path + DB.execute_ dbConn "PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;" + pure dbConn checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) @@ -121,63 +130,68 @@ handleSQLError err e | DB.sqlError e == DB.ErrorConstraint = err | otherwise = SEInternal $ bshow e -withTransaction :: forall a. DB.Connection -> IO a -> IO a -withTransaction db a = loop 100 100_000 +withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a +withConnection SQLiteStore {dbConnPool} = + bracket + (atomically $ readTBQueue dbConnPool) + (atomically . writeTBQueue dbConnPool) + +withTransaction :: forall a. SQLiteStore -> (DB.Connection -> IO a) -> IO a +withTransaction st action = withConnection st $ loop 100 100_000 where - loop :: Int -> Int -> IO a - loop t tLim = - DB.withImmediateTransaction db a `E.catch` \(e :: SQLError) -> + loop :: Int -> Int -> DB.Connection -> IO a + loop t tLim db = + DB.withImmediateTransaction db (action db) `E.catch` \(e :: SQLError) -> if tLim > t && DB.sqlError e == DB.ErrorBusy then do threadDelay t - loop (t * 9 `div` 8) (tLim - t) + loop (t * 9 `div` 8) (tLim - t) db else E.throwIO e instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteStore m where createRcvConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId - createRcvConn SQLiteStore {dbConn} gVar cData q@RcvQueue {server} = + createRcvConn st gVar cData q@RcvQueue {server} = -- TODO if schema has to be restarted, this function can be refactored -- to create connection first using createWithRandomId - liftIOEither . checkConstraint SEConnDuplicate . withTransaction dbConn $ - getConnId_ dbConn gVar cData >>= traverse create + liftIOEither . checkConstraint SEConnDuplicate . withTransaction st $ \db -> + getConnId_ db gVar cData >>= traverse (create db) where - create :: ConnId -> IO ConnId - create connId = do - upsertServer_ dbConn server - insertRcvQueue_ dbConn connId q - insertRcvConnection_ dbConn cData {connId} q + create :: DB.Connection -> ConnId -> IO ConnId + create db connId = do + upsertServer_ db server + insertRcvQueue_ db connId q + insertRcvConnection_ db cData {connId} q pure connId createSndConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId - createSndConn SQLiteStore {dbConn} gVar cData q@SndQueue {server} = + createSndConn st gVar cData q@SndQueue {server} = -- TODO if schema has to be restarted, this function can be refactored -- to create connection first using createWithRandomId - liftIOEither . checkConstraint SEConnDuplicate . withTransaction dbConn $ - getConnId_ dbConn gVar cData >>= traverse create + liftIOEither . checkConstraint SEConnDuplicate . withTransaction st $ \db -> + getConnId_ db gVar cData >>= traverse (create db) where - create :: ConnId -> IO ConnId - create connId = do - upsertServer_ dbConn server - insertSndQueue_ dbConn connId q - insertSndConnection_ dbConn cData {connId} q + create :: DB.Connection -> ConnId -> IO ConnId + create db connId = do + upsertServer_ db server + insertSndQueue_ db connId q + insertSndConnection_ db cData {connId} q pure connId getConn :: SQLiteStore -> ConnId -> m SomeConn - getConn SQLiteStore {dbConn} connId = - liftIOEither . withTransaction dbConn $ - getConn_ dbConn connId + getConn st connId = + liftIOEither . withTransaction st $ \db -> + getConn_ db connId getAllConnIds :: SQLiteStore -> m [ConnId] - getAllConnIds SQLiteStore {dbConn} = - liftIO $ do - r <- DB.query_ dbConn "SELECT conn_alias FROM connections;" :: IO [[ConnId]] - return (concat r) + getAllConnIds st = + liftIO . withConnection st $ \db -> do + concat <$> (DB.query_ db "SELECT conn_alias FROM connections;" :: IO [[ConnId]]) getRcvConn :: SQLiteStore -> SMPServer -> SMP.RecipientId -> m SomeConn - getRcvConn SQLiteStore {dbConn} SMPServer {host, port} rcvId = - liftIOEither . withTransaction dbConn $ + getRcvConn st SMPServer {host, port} rcvId = + liftIOEither . withTransaction st $ \db -> DB.queryNamed - dbConn + db [sql| SELECT q.conn_alias FROM rcv_queues q @@ -185,47 +199,47 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId] >>= \case - [Only connId] -> getConn_ dbConn connId + [Only connId] -> getConn_ db connId _ -> pure $ Left SEConnNotFound deleteConn :: SQLiteStore -> ConnId -> m () - deleteConn SQLiteStore {dbConn} connId = - liftIO $ + deleteConn st connId = + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db "DELETE FROM connections WHERE conn_alias = :conn_alias;" [":conn_alias" := connId] upgradeRcvConnToDuplex :: SQLiteStore -> ConnId -> SndQueue -> m () - upgradeRcvConnToDuplex SQLiteStore {dbConn} connId sq@SndQueue {server} = - liftIOEither . withTransaction dbConn $ - getConn_ dbConn connId >>= \case + upgradeRcvConnToDuplex st connId sq@SndQueue {server} = + liftIOEither . withTransaction st $ \db -> + getConn_ db connId >>= \case Right (SomeConn _ RcvConnection {}) -> do - upsertServer_ dbConn server - insertSndQueue_ dbConn connId sq - updateConnWithSndQueue_ dbConn connId sq + upsertServer_ db server + insertSndQueue_ db connId sq + updateConnWithSndQueue_ db connId sq pure $ Right () Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c _ -> pure $ Left SEConnNotFound upgradeSndConnToDuplex :: SQLiteStore -> ConnId -> RcvQueue -> m () - upgradeSndConnToDuplex SQLiteStore {dbConn} connId rq@RcvQueue {server} = - liftIOEither . withTransaction dbConn $ - getConn_ dbConn connId >>= \case + upgradeSndConnToDuplex st connId rq@RcvQueue {server} = + liftIOEither . withTransaction st $ \db -> + getConn_ db connId >>= \case Right (SomeConn _ SndConnection {}) -> do - upsertServer_ dbConn server - insertRcvQueue_ dbConn connId rq - updateConnWithRcvQueue_ dbConn connId rq + upsertServer_ db server + insertRcvQueue_ db connId rq + updateConnWithRcvQueue_ db connId rq pure $ Right () Right (SomeConn c _) -> pure . Left . SEBadConnType $ connType c _ -> pure $ Left SEConnNotFound setRcvQueueStatus :: SQLiteStore -> RcvQueue -> QueueStatus -> m () - setRcvQueueStatus SQLiteStore {dbConn} RcvQueue {rcvId, server = SMPServer {host, port}} status = + setRcvQueueStatus st RcvQueue {rcvId, server = SMPServer {host, port}} status = -- ? throw error if queue does not exist? - liftIO $ + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db [sql| UPDATE rcv_queues SET status = :status @@ -234,11 +248,11 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto [":status" := status, ":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId] setRcvQueueActive :: SQLiteStore -> RcvQueue -> VerificationKey -> m () - setRcvQueueActive SQLiteStore {dbConn} RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey = + setRcvQueueActive st RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey = -- ? throw error if queue does not exist? - liftIO $ + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db [sql| UPDATE rcv_queues SET verify_key = :verify_key, status = :status @@ -252,11 +266,11 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto ] setSndQueueStatus :: SQLiteStore -> SndQueue -> QueueStatus -> m () - setSndQueueStatus SQLiteStore {dbConn} SndQueue {sndId, server = SMPServer {host, port}} status = + setSndQueueStatus st SndQueue {sndId, server = SMPServer {host, port}} status = -- ? throw error if queue does not exist? - liftIO $ + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db [sql| UPDATE snd_queues SET status = :status @@ -265,57 +279,58 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto [":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] updateRcvIds :: SQLiteStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) - updateRcvIds SQLiteStore {dbConn} connId = - liftIO . withTransaction dbConn $ do - (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ dbConn connId + updateRcvIds st connId = + liftIO . withTransaction st $ \db -> do + (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ db connId let internalId = InternalId $ unId lastInternalId + 1 internalRcvId = InternalRcvId $ unRcvId lastInternalRcvId + 1 - updateLastIdsRcv_ dbConn connId internalId internalRcvId + updateLastIdsRcv_ db connId internalId internalRcvId pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash) createRcvMsg :: SQLiteStore -> ConnId -> RcvMsgData -> m () - createRcvMsg SQLiteStore {dbConn} connId rcvMsgData = - liftIO . withTransaction dbConn $ do - insertRcvMsgBase_ dbConn connId rcvMsgData - insertRcvMsgDetails_ dbConn connId rcvMsgData - updateHashRcv_ dbConn connId rcvMsgData + createRcvMsg st connId rcvMsgData = + liftIO . withTransaction st $ \db -> do + insertRcvMsgBase_ db connId rcvMsgData + insertRcvMsgDetails_ db connId rcvMsgData + updateHashRcv_ db connId rcvMsgData updateSndIds :: SQLiteStore -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) - updateSndIds SQLiteStore {dbConn} connId = - liftIO . withTransaction dbConn $ do - (lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ dbConn connId + updateSndIds st connId = + liftIO . withTransaction st $ \db -> do + (lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ db connId let internalId = InternalId $ unId lastInternalId + 1 internalSndId = InternalSndId $ unSndId lastInternalSndId + 1 - updateLastIdsSnd_ dbConn connId internalId internalSndId + updateLastIdsSnd_ db connId internalId internalSndId pure (internalId, internalSndId, prevSndHash) createSndMsg :: SQLiteStore -> ConnId -> SndMsgData -> m () - createSndMsg SQLiteStore {dbConn} connId sndMsgData = - liftIO . withTransaction dbConn $ do - insertSndMsgBase_ dbConn connId sndMsgData - insertSndMsgDetails_ dbConn connId sndMsgData - updateHashSnd_ dbConn connId sndMsgData + createSndMsg st connId sndMsgData = + liftIO . withTransaction st $ \db -> do + insertSndMsgBase_ db connId sndMsgData + insertSndMsgDetails_ db connId sndMsgData + updateHashSnd_ db connId sndMsgData getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg getMsg _st _connAlias _id = throwError SENotImplemented createIntro :: SQLiteStore -> TVar ChaChaDRG -> NewIntroduction -> m IntroId - createIntro SQLiteStore {dbConn} gVar NewIntroduction {toConn, reConn, reInfo} = - liftIOEither . createWithRandomId gVar $ \introId -> - DB.execute - dbConn - [sql| - INSERT INTO conn_intros - (intro_id, to_conn, re_conn, re_info) VALUES (?, ?, ?, ?); - |] - (introId, toConn, reConn, reInfo) + createIntro st gVar NewIntroduction {toConn, reConn, reInfo} = + liftIOEither . withConnection st $ \db -> + createWithRandomId gVar $ \introId -> + DB.execute + db + [sql| + INSERT INTO conn_intros + (intro_id, to_conn, re_conn, re_info) VALUES (?, ?, ?, ?); + |] + (introId, toConn, reConn, reInfo) getIntro :: SQLiteStore -> IntroId -> m Introduction - getIntro SQLiteStore {dbConn} introId = - liftIOEither $ + getIntro st introId = + liftIOEither . withConnection st $ \db -> intro <$> DB.query - dbConn + db [sql| SELECT to_conn, to_info, to_status, re_conn, re_info, re_status, queue_info FROM conn_intros @@ -328,10 +343,10 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto intro _ = Left SEIntroNotFound addIntroInvitation :: SQLiteStore -> IntroId -> ConnInfo -> SMPQueueInfo -> m () - addIntroInvitation SQLiteStore {dbConn} introId toInfo qInfo = - liftIO $ + addIntroInvitation st introId toInfo qInfo = + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db [sql| UPDATE conn_intros SET to_info = :to_info, @@ -346,10 +361,10 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto ] setIntroToStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () - setIntroToStatus SQLiteStore {dbConn} introId toStatus = - liftIO $ + setIntroToStatus st introId toStatus = + liftIO . withConnection st $ \db -> DB.execute - dbConn + db [sql| UPDATE conn_intros SET to_status = ? @@ -358,10 +373,10 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto (toStatus, introId) setIntroReStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () - setIntroReStatus SQLiteStore {dbConn} introId reStatus = - liftIO $ + setIntroReStatus st introId reStatus = + liftIO . withConnection st $ \db -> DB.execute - dbConn + db [sql| UPDATE conn_intros SET re_status = ? @@ -370,22 +385,23 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto (reStatus, introId) createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId - createInvitation SQLiteStore {dbConn} gVar NewInvitation {viaConn, externalIntroId, connInfo, qInfo} = - liftIOEither . createWithRandomId gVar $ \invId -> - DB.execute - dbConn - [sql| - INSERT INTO conn_invitations - (inv_id, via_conn, external_intro_id, conn_info, queue_info) VALUES (?, ?, ?, ?, ?); - |] - (invId, viaConn, externalIntroId, connInfo, qInfo) + createInvitation st gVar NewInvitation {viaConn, externalIntroId, connInfo, qInfo} = + liftIOEither . withConnection st $ \db -> + createWithRandomId gVar $ \invId -> + DB.execute + db + [sql| + INSERT INTO conn_invitations + (inv_id, via_conn, external_intro_id, conn_info, queue_info) VALUES (?, ?, ?, ?, ?); + |] + (invId, viaConn, externalIntroId, connInfo, qInfo) getInvitation :: SQLiteStore -> InvitationId -> m Invitation - getInvitation SQLiteStore {dbConn} invId = - liftIOEither $ + getInvitation st invId = + liftIOEither . withConnection st $ \db -> invitation <$> DB.query - dbConn + db [sql| SELECT via_conn, external_intro_id, conn_info, queue_info, conn_id, status FROM conn_invitations @@ -398,10 +414,10 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto invitation _ = Left SEInvitationNotFound addInvitationConn :: SQLiteStore -> InvitationId -> ConnId -> m () - addInvitationConn SQLiteStore {dbConn} invId connId = - liftIO $ + addInvitationConn st invId connId = + liftIO . withConnection st $ \db -> DB.executeNamed - dbConn + db [sql| UPDATE conn_invitations SET conn_id = :conn_id, status = :status @@ -410,32 +426,32 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto [":conn_id" := connId, ":status" := InvAcpt, ":inv_id" := invId] getConnInvitation :: SQLiteStore -> ConnId -> m (Maybe (Invitation, Connection 'CDuplex)) - getConnInvitation SQLiteStore {dbConn} cId = - liftIO . withTransaction dbConn $ + getConnInvitation st cId = + liftIO . withTransaction st $ \db -> DB.query - dbConn + db [sql| SELECT inv_id, via_conn, external_intro_id, conn_info, queue_info, status FROM conn_invitations WHERE conn_id = ?; |] (Only cId) - >>= fmap join . traverse getViaConn . invitation + >>= fmap join . traverse (getViaConn db) . invitation where invitation [(invId, viaConn, externalIntroId, connInfo, qInfo, status)] = Just $ Invitation {invId, viaConn, externalIntroId, connInfo, qInfo, connId = Just cId, status} invitation _ = Nothing - getViaConn :: Invitation -> IO (Maybe (Invitation, Connection 'CDuplex)) - getViaConn inv@Invitation {viaConn} = fmap (inv,) . duplexConn <$> getConn_ dbConn viaConn + getViaConn :: DB.Connection -> Invitation -> IO (Maybe (Invitation, Connection 'CDuplex)) + getViaConn db inv@Invitation {viaConn} = fmap (inv,) . duplexConn <$> getConn_ db viaConn duplexConn :: Either StoreError SomeConn -> Maybe (Connection 'CDuplex) duplexConn (Right (SomeConn SCDuplex conn)) = Just conn duplexConn _ = Nothing setInvitationStatus :: SQLiteStore -> InvitationId -> InvitationStatus -> m () - setInvitationStatus SQLiteStore {dbConn} invId status = - liftIO $ + setInvitationStatus st invId status = + liftIO . withConnection st $ \db -> DB.execute - dbConn + db [sql| UPDATE conn_invitations SET status = ? WHERE inv_id = ?; diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index c3c1bcdd2..1a00df797 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -8,7 +9,7 @@ module AgentTests.SQLiteTests (storeTests) where import Control.Concurrent.Async (concurrently_) -import Control.Concurrent.STM (newTVarIO) +import Control.Concurrent.STM import Control.Monad (replicateM_) import Control.Monad.Except (ExceptT, runExceptT) import qualified Crypto.PubKey.RSA as R @@ -42,7 +43,7 @@ withStore2 = before connect2 . after (removeStore . fst) connect2 :: IO (SQLiteStore, SQLiteStore) connect2 = do s1 <- createStore - s2 <- connectSQLiteStore $ dbFilePath s1 + s2 <- connectSQLiteStore (dbFilePath s1) 4 pure (s1, s2) createStore :: IO SQLiteStore @@ -50,12 +51,15 @@ createStore = do -- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous -- IO operations on multiple similarly named files; error seems to be environment specific r <- randomIO :: IO Word32 - createSQLiteStore (testDB <> show r) Migrations.app + createSQLiteStore (testDB <> show r) 4 Migrations.app removeStore :: SQLiteStore -> IO () removeStore store = do - DB.close $ dbConn store + close store removeFile $ dbFilePath store + where + close :: SQLiteStore -> IO () + close st = mapM_ DB.close =<< atomically (flushTBQueue $ dbConnPool st) returnsResult :: (Eq a, Eq e, Show a, Show e) => ExceptT e IO a -> a -> Expectation action `returnsResult` r = runExceptT action `shouldReturn` Right r @@ -122,13 +126,16 @@ testConcurrentWrites = testCompiledThreadsafe :: SpecWith SQLiteStore testCompiledThreadsafe = - it "compiled sqlite library should be threadsafe" $ \store -> do - compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]] + it "compiled sqlite library should be threadsafe" . withStoreConnection $ \db -> do + compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[T.Text]] compileOptions `shouldNotContain` [["THREADSAFE=0"]] +withStoreConnection :: (DB.Connection -> IO a) -> SQLiteStore -> IO a +withStoreConnection = flip withConnection + testForeignKeysEnabled :: SpecWith SQLiteStore testForeignKeysEnabled = - it "foreign keys should be enabled" $ \store -> do + it "foreign keys should be enabled" . withStoreConnection $ \db -> do let inconsistentQuery = [sql| INSERT INTO connections @@ -136,7 +143,7 @@ testForeignKeysEnabled = VALUES ("conn1", "smp.simplex.im", "5223", "1234", "smp.simplex.im", "5223", "2345"); |] - DB.execute_ (dbConn store) inconsistentQuery + DB.execute_ db inconsistentQuery `shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint) cData1 :: ConnData diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 918b276f0..f30702cf6 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -148,6 +148,7 @@ cfg = connIdBytes = 12, tbqSize = 1, dbFile = testDB, + dbPoolSize = 4, smpCfg = smpDefaultConfig { qSize = 1, From e4d9b481ec53bcbc0a6084a0f19246683fdbf0fb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 30 Jun 2021 10:29:45 +0100 Subject: [PATCH 06/29] refactor message meta to MsgMeta type (#164) --- src/Simplex/Messaging/Agent.hs | 30 +++-------- src/Simplex/Messaging/Agent/Protocol.hs | 56 +++++++++++---------- src/Simplex/Messaging/Agent/Store.hs | 10 ++-- src/Simplex/Messaging/Agent/Store/SQLite.hs | 22 ++++---- tests/AgentTests.hs | 2 +- tests/AgentTests/SQLiteTests.hs | 19 ++++--- 6 files changed, 64 insertions(+), 75 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 5c455ba9a..b27aeab14 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -534,34 +534,18 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do sendConMsg toConn reConn = atomically $ writeTBQueue subQ ("", toConn, ICON reConn) agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () - agentClientMsg receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do + agentClientMsg externalPrevSndHash sender broker msgBody internalHash = do logServer "<--" c srv rId "MSG " case status of Active -> do internalTs <- liftIO getCurrentTime (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId) - let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash receivedPrevMsgHash - withStore $ \st -> - createRcvMsg st connId $ - RcvMsgData - { internalId, - internalRcvId, - internalTs, - senderMeta, - brokerMeta, - msgBody, - internalHash = msgHash, - externalPrevSndHash = receivedPrevMsgHash, - msgIntegrity - } - notify - MSG - { recipientMeta = (unId internalId, internalTs), - senderMeta, - brokerMeta, - msgBody, - msgIntegrity - } + let integrity = checkMsgIntegrity prevExtSndId (fst sender) prevRcvMsgHash externalPrevSndHash + recipient = (unId internalId, internalTs) + msgMeta = MsgMeta {integrity, recipient, sender, broker} + rcvMsg = RcvMsgData {msgMeta, msgBody, internalRcvId, internalHash, externalPrevSndHash} + withStore $ \st -> createRcvMsg st connId rcvMsg + notify $ MSG msgMeta msgBody _ -> prohibited checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index ab57e3d5e..0b81b6f9b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -32,6 +32,7 @@ module Simplex.Messaging.Agent.Protocol ACommand (..), AParty (..), SAParty (..), + MsgMeta (..), SMPMessage (..), AMessage (..), SMPServer (..), @@ -166,14 +167,7 @@ data ACommand (p :: AParty) where -- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent SEND :: MsgBody -> ACommand Client SENT :: AgentMsgId -> ACommand Agent - MSG :: - { recipientMeta :: (AgentMsgId, UTCTime), - brokerMeta :: (MsgId, UTCTime), - senderMeta :: (AgentMsgId, UTCTime), - msgIntegrity :: MsgIntegrity, - msgBody :: MsgBody - } -> - ACommand Agent + MSG :: MsgMeta -> MsgBody -> ACommand Agent -- ACK :: AgentMsgId -> ACommand Client -- RCVD :: AgentMsgId -> ACommand Agent OFF :: ACommand Client @@ -185,6 +179,15 @@ deriving instance Eq (ACommand p) deriving instance Show (ACommand p) +-- | Agent message metadata sent to the client +data MsgMeta = MsgMeta + { integrity :: MsgIntegrity, + recipient :: (AgentMsgId, UTCTime), + broker :: (MsgId, UTCTime), + sender :: (AgentMsgId, UTCTime) + } + deriving (Eq, Show) + -- | SMP message formats. data SMPMessage = -- | SMP confirmation @@ -496,16 +499,16 @@ commandP = sendCmd = ACmd SClient . SEND <$> A.takeByteString sentResp = ACmd SAgent . SENT <$> A.decimal iconMsg = ACmd SAgent . ICON <$> A.takeTill wordEnd - message = do - msgIntegrity <- msgIntegrityP <* A.space - recipientMeta <- "R=" *> partyMeta A.decimal - brokerMeta <- "B=" *> partyMeta base64P - senderMeta <- "S=" *> partyMeta A.decimal - msgBody <- A.takeByteString - return $ ACmd SAgent MSG {recipientMeta, brokerMeta, senderMeta, msgIntegrity, msgBody} + message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString) + msgMetaP = do + integrity <- msgIntegrityP + recipient <- " R=" *> partyMeta A.decimal + broker <- " B=" *> partyMeta base64P + sender <- " S=" *> partyMeta A.decimal + pure MsgMeta {integrity, recipient, broker, sender} introP f = f <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On) - partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space + partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P agentError = ACmd SAgent . ERR <$> agentErrorTypeP -- | Message integrity validation result parser. @@ -534,15 +537,8 @@ serializeCommand = \case END -> "END" SEND msgBody -> "SEND " <> serializeMsg msgBody SENT mId -> "SENT " <> bshow mId - MSG {recipientMeta = (rmId, rTs), brokerMeta = (bmId, bTs), senderMeta = (smId, sTs), msgIntegrity, msgBody} -> - B.unwords - [ "MSG", - serializeMsgIntegrity msgIntegrity, - "R=" <> bshow rmId <> "," <> showTs rTs, - "B=" <> encode bmId <> "," <> showTs bTs, - "S=" <> bshow smId <> "," <> showTs sTs, - serializeMsg msgBody - ] + MSG msgMeta msgBody -> + "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeMsg msgBody OFF -> "OFF" DEL -> "DEL" CON -> "CON" @@ -556,6 +552,14 @@ serializeCommand = \case ReplyMode On -> "" showTs :: UTCTime -> ByteString showTs = B.pack . formatISO8601Millis + serializeMsgMeta :: MsgMeta -> ByteString + serializeMsgMeta MsgMeta {integrity, recipient = (rmId, rTs), broker = (bmId, bTs), sender = (smId, sTs)} = + B.unwords + [ serializeMsgIntegrity integrity, + "R=" <> bshow rmId <> "," <> showTs rTs, + "B=" <> encode bmId <> "," <> showTs bTs, + "S=" <> bshow smId <> "," <> showTs sTs + ] -- | Serialize message integrity validation result. serializeMsgIntegrity :: MsgIntegrity -> ByteString @@ -636,7 +640,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p)) cmdWithMsgBody = \case SEND body -> SEND <$$> getMsgBody body - MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body + MSG msgMeta body -> MSG msgMeta <$$> getMsgBody body INTRO introId cInfo -> INTRO introId <$$> getMsgBody cInfo REQ introId cInfo -> REQ introId <$$> getMsgBody cInfo ACPT introId cInfo -> ACPT introId <$$> getMsgBody cInfo diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 7b4b03cbc..cad975570 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -170,15 +170,11 @@ type PrevSndMsgHash = MsgHash -- * Message data containers - used on Msg creation to reduce number of parameters data RcvMsgData = RcvMsgData - { internalId :: InternalId, - internalRcvId :: InternalRcvId, - internalTs :: InternalTs, - senderMeta :: (ExternalSndId, ExternalSndTs), - brokerMeta :: (BrokerId, BrokerTs), + { msgMeta :: MsgMeta, msgBody :: MsgBody, + internalRcvId :: InternalRcvId, internalHash :: MsgHash, - externalPrevSndHash :: MsgHash, - msgIntegrity :: MsgIntegrity + externalPrevSndHash :: MsgHash } data SndMsgData = SndMsgData diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index ce46d964c..3d3ec8d67 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -746,7 +746,8 @@ updateLastIdsRcv_ dbConn connId newInternalId newInternalRcvId = -- * createRcvMsg helpers insertRcvMsgBase_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () -insertRcvMsgBase_ dbConn connId RcvMsgData {..} = do +insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgBody, internalRcvId} = do + let MsgMeta {recipient = (internalId, internalTs)} = msgMeta DB.executeNamed dbConn [sql| @@ -763,7 +764,8 @@ insertRcvMsgBase_ dbConn connId RcvMsgData {..} = do ] insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () -insertRcvMsgDetails_ dbConn connId RcvMsgData {..} = +insertRcvMsgDetails_ dbConn connId RcvMsgData {msgMeta, internalRcvId, internalHash, externalPrevSndHash} = do + let MsgMeta {integrity, recipient, sender, broker} = msgMeta DB.executeNamed dbConn [sql| @@ -778,19 +780,19 @@ insertRcvMsgDetails_ dbConn connId RcvMsgData {..} = |] [ ":conn_alias" := connId, ":internal_rcv_id" := internalRcvId, - ":internal_id" := internalId, - ":external_snd_id" := fst senderMeta, - ":external_snd_ts" := snd senderMeta, - ":broker_id" := fst brokerMeta, - ":broker_ts" := snd brokerMeta, + ":internal_id" := fst recipient, + ":external_snd_id" := fst sender, + ":external_snd_ts" := snd sender, + ":broker_id" := fst broker, + ":broker_ts" := snd broker, ":rcv_status" := Received, ":internal_hash" := internalHash, ":external_prev_snd_hash" := externalPrevSndHash, - ":integrity" := msgIntegrity + ":integrity" := integrity ] updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () -updateHashRcv_ dbConn connId RcvMsgData {..} = +updateHashRcv_ dbConn connId RcvMsgData {msgMeta, internalHash, internalRcvId} = DB.executeNamed dbConn -- last_internal_rcv_msg_id equality check prevents race condition in case next id was reserved @@ -801,7 +803,7 @@ updateHashRcv_ dbConn connId RcvMsgData {..} = WHERE conn_alias = :conn_alias AND last_internal_rcv_msg_id = :last_internal_rcv_msg_id; |] - [ ":last_external_snd_msg_id" := fst senderMeta, + [ ":last_external_snd_msg_id" := fst (sender msgMeta), ":last_rcv_msg_hash" := internalHash, ":conn_alias" := connId, ":last_internal_rcv_msg_id" := internalRcvId diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 34993c8e8..576c2ea63 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -101,7 +101,7 @@ h #:# err = tryGet `shouldReturn` () _ -> return () pattern Msg :: MsgBody -> ACommand 'Agent -pattern Msg msgBody <- MSG {msgBody, msgIntegrity = MsgOk} +pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 1a00df797..6dd69da19 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -402,21 +402,24 @@ ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0) mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash = RcvMsgData - { internalId, - internalRcvId, - internalTs = ts, - senderMeta = (externalSndId, ts), - brokerMeta = (brokerId, ts), + { internalRcvId, + msgMeta = + MsgMeta + { integrity = MsgOk, + recipient = (unId internalId, ts), + sender = (externalSndId, ts), + broker = (brokerId, ts) + }, msgBody = hw, internalHash, - externalPrevSndHash = "hash_from_sender", - msgIntegrity = MsgOk + externalPrevSndHash = "hash_from_sender" } testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation testCreateRcvMsg' st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do + let MsgMeta {recipient = (internalId, _)} = msgMeta updateRcvIds st connId - `returnsResult` (internalId, internalRcvId, expectedPrevSndId, expectedPrevHash) + `returnsResult` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash) createRcvMsg st connId rcvMsgData `returnsResult` () From daad3315ebe66f13d41ae5ac30fc3d189d6186ef Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 30 Jun 2021 10:35:50 +0100 Subject: [PATCH 07/29] upgrade stack resolver to lts-18.0 (#165) * upgrade stack resolver to lts-18.0 * fix random package version --- package.yaml | 13 ++++++------- stack.yaml | 2 +- tests/AgentTests/SQLiteTests.hs | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package.yaml b/package.yaml index ce4f42a9c..88889931c 100644 --- a/package.yaml +++ b/package.yaml @@ -22,7 +22,7 @@ extra-source-files: - CHANGELOG.md dependencies: - - ansi-terminal == 0.10.* + - ansi-terminal >= 0.10 && < 0.12 - asn1-encoding == 0.9.* - asn1-types == 0.3.* - async == 2.2.* @@ -31,21 +31,21 @@ dependencies: - base64-bytestring >= 1.0 && < 1.3 - bytestring == 0.10.* - composition == 1.0.* - - constraints == 0.12.* + - constraints >= 0.12 && < 0.14 - containers == 0.6.* - - cryptonite == 0.27.* + - cryptonite >= 0.27 && < 0.30 - direct-sqlite == 2.3.* - directory == 1.3.* - file-embed == 0.0.14.* - filepath == 1.4.* - - generic-random == 1.3.* + - generic-random >= 1.3 && < 1.5 - iso8601-time == 0.1.* - memory == 0.15.* - mtl == 2.2.* - network == 3.1.* - network-transport == 0.5.* - QuickCheck == 2.14.* - - random == 1.1.* + - random >= 1.1 && < 1.3 - simple-logger == 0.1.* - sqlite-simple == 0.4.* - stm == 2.5.* @@ -68,7 +68,7 @@ executables: dependencies: - cryptostore == 0.2.* - ini == 0.4.* - - optparse-applicative == 0.15.* + - optparse-applicative >= 0.15 && < 0.17 - simplexmq ghc-options: - -threaded @@ -90,7 +90,6 @@ tests: - hspec == 2.7.* - hspec-core == 2.7.* - HUnit == 1.6.* - - random == 1.1.* - QuickCheck == 2.14.* - timeit == 2.0.* diff --git a/stack.yaml b/stack.yaml index ae97d2a94..70267dd80 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,7 +17,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-17.12 +resolver: lts-18.0 # User packages to be built. # Various formats can be used as shown in the example below. diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 6dd69da19..56fd88514 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -27,7 +27,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C -import System.Random (Random (randomIO)) +import System.Random import Test.Hspec import UnliftIO.Directory (removeFile) From 3d9ceff691c8a5ac05b7c9ea18fcc3b707eafeb8 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sun, 4 Jul 2021 04:48:24 +1000 Subject: [PATCH 08/29] ask client for confirmation of sender; make establishment of connection asynchronous (#163) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/smp-server/Main.hs | 2 +- migrations/20210624_confirmations.sql | 9 + src/Simplex/Messaging/Agent.hs | 202 ++++++++++++++------ src/Simplex/Messaging/Agent/Client.hs | 67 +++++-- src/Simplex/Messaging/Agent/Protocol.hs | 101 +++++----- src/Simplex/Messaging/Agent/Store.hs | 28 ++- src/Simplex/Messaging/Agent/Store/SQLite.hs | 93 ++++++++- src/Simplex/Messaging/Crypto.hs | 39 +++- src/Simplex/Messaging/Server/Env/STM.hs | 2 +- tests/AgentTests.hs | 49 +++-- tests/AgentTests/SQLiteTests.hs | 6 +- 11 files changed, 446 insertions(+), 152 deletions(-) create mode 100644 migrations/20210624_confirmations.sql diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index c0802a66b..05161bde1 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -222,7 +222,7 @@ confirm msg = do when (map toLower ok /= "y") exitFailure serverKeyHash :: C.FullPrivateKey -> B.ByteString -serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey +serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey' openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode)) openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f} diff --git a/migrations/20210624_confirmations.sql b/migrations/20210624_confirmations.sql new file mode 100644 index 000000000..f7b1e8e85 --- /dev/null +++ b/migrations/20210624_confirmations.sql @@ -0,0 +1,9 @@ +CREATE TABLE conn_confirmations ( + confirmation_id BLOB NOT NULL PRIMARY KEY, + conn_alias BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, + sender_key BLOB NOT NULL, + sender_conn_info BLOB NOT NULL, + accepted INTEGER NOT NULL, + own_conn_info BLOB, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +) WITHOUT ROWID; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index b27aeab14..2581a9dc0 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -6,6 +6,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -39,6 +40,7 @@ module Simplex.Messaging.Agent getSMPAgentClient, createConnection, joinConnection, + allowConnection, sendIntroduction, acceptInvitation, subscribeConnection, @@ -47,6 +49,7 @@ module Simplex.Messaging.Agent deleteConnection, createConnection', joinConnection', + allowConnection', sendIntroduction', acceptInvitation', subscribeConnection', @@ -111,7 +114,7 @@ runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReader c <- getAgentClient logConnection c True race_ (connectClient h c) (runAgentClient c) - `E.finally` disconnectServers c + `E.finally` disconnectAgentClient c -- | Creates an SMP agent client instance getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m AgentClient @@ -119,11 +122,11 @@ getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent where runAgent = do c <- getAgentClient - action <- async $ subscriber c `E.finally` disconnectServers c + action <- async $ subscriber c `E.finally` disconnectAgentClient c pure c {smpSubscriber = action} -disconnectServers :: MonadUnliftIO m => AgentClient -> m () -disconnectServers c = closeSMPServerClients c >> logConnection c False +disconnectAgentClient :: MonadUnliftIO m => AgentClient -> m () +disconnectAgentClient c = closeAgentClient c >> logConnection c False -- | type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) @@ -137,12 +140,16 @@ createConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> m (ConnI createConnection c = (`runReaderT` agentEnv c) . createConnection' c -- | Join SMP agent connection (JOIN command) in Reader monad -joinConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> m ConnId -joinConnection' c connId qInfo = joinConn c (fromMaybe "" connId) qInfo (ReplyMode On) Nothing 0 +joinConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConnection' c connId qInfo cInfo = joinConn c (fromMaybe "" connId) qInfo cInfo Nothing 0 -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> m ConnId -joinConnection c = (`runReaderT` agentEnv c) .: joinConnection' c +joinConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConnection c = (`runReaderT` agentEnv c) .:. joinConnection' c + +-- | Approve confirmation (LET command) +allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +allowConnection c = (`runReaderT` agentEnv c) .:. allowConnection' c -- | Accept invitation (ACPT command) in Reader monad acceptInvitation' :: AgentMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId @@ -150,7 +157,7 @@ acceptInvitation' c = acceptInv c "" -- | Accept invitation (ACPT command) acceptInvitation :: AgentErrorMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId -acceptInvitation c = (`runReaderT` agentEnv c) .: acceptInvitation c +acceptInvitation c = (`runReaderT` agentEnv c) .: acceptInvitation' c -- | Send introduction of the second connection the first (INTRO command) sendIntroduction :: AgentErrorMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () @@ -244,7 +251,8 @@ withStore action = do processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent) processCommand c (connId, cmd) = case cmd of NEW -> second INV <$> newConn c connId Nothing 0 - JOIN smpQueueInfo replyMode -> (,OK) <$> joinConn c connId smpQueueInfo replyMode Nothing 0 + JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo Nothing 0 + LET confId ownConnInfo -> allowConnection' c connId confId ownConnInfo $> (connId, OK) INTRO reConnId reInfo -> sendIntroduction' c connId reConnId reInfo $> (connId, OK) ACPT invId connInfo -> (,OK) <$> acceptInv c connId invId connInfo SUB -> subscribeConnection' c connId $> (connId, OK) @@ -255,31 +263,69 @@ processCommand c (connId, cmd) = case cmd of newConn :: AgentMonad m => AgentClient -> ConnId -> Maybe InvitationId -> Int -> m (ConnId, SMPQueueInfo) newConn c connId viaInv connLevel = do srv <- getSMPServer - (rq, qInfo) <- newReceiveQueue c srv + (rq, qInfo) <- newRcvQueue c srv g <- asks idsDrg let cData = ConnData {connId, viaInv, connLevel} connId' <- withStore $ \st -> createRcvConn st g cData rq addSubscription c rq connId' pure (connId', qInfo) -joinConn :: forall m. AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ReplyMode -> Maybe InvitationId -> Int -> m ConnId -joinConn c connId qInfo (ReplyMode replyMode) viaInv connLevel = do - (sq, senderKey, verifyKey) <- newSendQueue qInfo +minute :: Int +minute = 60_000_000 + +onlineInterval :: RetryInterval +onlineInterval = + RetryInterval + { initialInterval = 1_000_000, + increaseAfter = minute, + maxInterval = 10 * minute + } + +resumeInterval :: RetryInterval +resumeInterval = + RetryInterval + { initialInterval = 5_000_000, + increaseAfter = 0, + maxInterval = 10 * minute + } + +joinConn :: AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ConnInfo -> Maybe InvitationId -> Int -> m ConnId +joinConn c connId qInfo cInfo viaInv connLevel = do + (sq, senderKey, verifyKey) <- newSndQueue qInfo g <- asks idsDrg let cData = ConnData {connId, viaInv, connLevel} connId' <- withStore $ \st -> createSndConn st g cData sq - connectToSendQueue c sq senderKey verifyKey - when (replyMode == On) $ createReplyQueue connId' sq + confirmQueue c sq senderKey cInfo + activateQueueJoining c connId' sq verifyKey onlineInterval pure connId' + +activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () +activateQueueJoining c connId sq verifyKey retryInterval = + activateQueue c connId sq verifyKey retryInterval createReplyQueue where - createReplyQueue :: ConnId -> SndQueue -> m () - createReplyQueue cId sq = do + createReplyQueue :: m () + createReplyQueue = do srv <- getSMPServer - (rq, qInfo') <- newReceiveQueue c srv - addSubscription c rq cId - withStore $ \st -> upgradeSndConnToDuplex st cId rq + (rq, qInfo') <- newRcvQueue c srv + addSubscription c rq connId + withStore $ \st -> upgradeSndConnToDuplex st connId rq sendControlMessage c sq $ REPLY qInfo' +-- | Approve confirmation (LET command) in Reader monad +allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +allowConnection' c connId confId ownConnInfo = + withStore (`getConn` connId) >>= \case + SomeConn SCRcv (RcvConnection _ rq) -> do + AcceptedConfirmation {senderKey} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo + processConfirmation c rq senderKey + _ -> throwError $ CMD PROHIBITED + +processConfirmation :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () +processConfirmation c rq sndKey = do + withStore $ \st -> setRcvQueueStatus st rq Confirmed + secureQueue c rq sndKey + withStore $ \st -> setRcvQueueStatus st rq Secured + -- | Send introduction of the second connection the first (INTRO command) in Reader monad sendIntroduction' :: AgentMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () sendIntroduction' c toConn reConn reInfo = @@ -302,19 +348,40 @@ acceptInv c connId invId connInfo = sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo pure connId' Just qInfo' -> do - connId' <- joinConn c connId qInfo' (ReplyMode On) (Just invId) (connLevel + 1) + -- TODO remove invitations from protocol + connId' <- joinConn c connId qInfo' connInfo (Just invId) (connLevel + 1) withStore $ \st -> addInvitationConn st invId connId' pure connId' _ -> throwError $ CONN SIMPLEX _ -> throwError $ CMD PROHIBITED -- | Subscribe to receive connection messages (SUB command) in Reader monad -subscribeConnection' :: AgentMonad m => AgentClient -> ConnId -> m () +subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = withStore (`getConn` connId) >>= \case - SomeConn _ (DuplexConnection _ rq _) -> subscribeQueue c rq connId + SomeConn _ (DuplexConnection _ rq sq) -> case status (sq :: SndQueue) of + Confirmed -> withVerifyKey sq $ \sndKey -> do + secureQueue c rq sndKey + withStore $ \st -> setRcvQueueStatus st rq Secured + activateSecuredQueue rq sq sndKey + Secured -> withVerifyKey sq $ activateSecuredQueue rq sq + Active -> subscribeQueue c rq connId + _ -> throwError $ INTERNAL "unexpected queue status" + SomeConn _ (SndConnection _ sq) -> case status (sq :: SndQueue) of + Confirmed -> withVerifyKey sq $ \sndKey -> + activateQueueJoining c connId sq sndKey resumeInterval + Active -> throwError $ CONN SIMPLEX + _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId - _ -> throwError $ CONN SIMPLEX + where + withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m () + withVerifyKey sq action = + let err = throwError $ INTERNAL "missing send queue public key" + in maybe err action . C.publicKey $ sndPrivateKey sq + activateSecuredQueue :: RcvQueue -> SndQueue -> C.PublicKey -> m () + activateSecuredQueue rq sq sndKey = do + activateQueueInitiating c connId sq sndKey resumeInterval + subscribeQueue c rq connId -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId @@ -408,7 +475,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do let msgHash = C.sha256Hash msg case parseSMPMessage msg of Left e -> notify $ ERR e - Right (SMPConfirmation senderKey) -> smpConfirmation senderKey + Right (SMPConfirmation senderKey cInfo) -> smpConfirmation senderKey cInfo Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> case agentMessage of HELLO verifyKey _ -> helloMsg verifyKey msgBody @@ -434,17 +501,20 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do prohibited :: m () prohibited = notify . ERR $ AGENT A_PROHIBITED - smpConfirmation :: SenderPublicKey -> m () - smpConfirmation senderKey = do + smpConfirmation :: SenderPublicKey -> ConnInfo -> m () + smpConfirmation senderKey cInfo = do logServer "<--" c srv rId "MSG " case status of - New -> do - -- TODO currently it automatically allows whoever sends the confirmation - -- TODO create invitation and send REQ - withStore $ \st -> setRcvQueueStatus st rq Confirmed - -- TODO update sender key in the store? - secureQueue c rq senderKey - withStore $ \st -> setRcvQueueStatus st rq Secured + New -> case cType of + SCRcv -> do + g <- asks idsDrg + let newConfirmation = NewConfirmation {connId, senderKey, senderConnInfo = cInfo} + confId <- withStore $ \st -> createConfirmation st g newConfirmation + notify $ CONF confId cInfo + SCDuplex -> do + notify $ INFO cInfo + processConfirmation c rq senderKey + _ -> prohibited _ -> prohibited helloMsg :: SenderPublicKey -> ByteString -> m () @@ -456,7 +526,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do void $ verifyMessage (Just verifyKey) msgBody withStore $ \st -> setRcvQueueActive st rq verifyKey case cType of - SCDuplex -> connected + SCDuplex -> notifyConnected c connId _ -> pure () replyMsg :: SMPQueueInfo -> m () @@ -464,21 +534,14 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do logServer "<--" c srv rId "MSG " case cType of SCRcv -> do - (sq, senderKey, verifyKey) <- newSendQueue qInfo + AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId) + (sq, senderKey, verifyKey) <- newSndQueue qInfo withStore $ \st -> upgradeRcvConnToDuplex st connId sq - connectToSendQueue c sq senderKey verifyKey - connected + confirmQueue c sq senderKey ownConnInfo + withStore (`removeConfirmations` connId) + activateQueueInitiating c connId sq verifyKey onlineInterval _ -> prohibited - connected :: m () - connected = do - withStore (`getConnInvitation` connId) >>= \case - Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do - withStore $ \st -> setInvitationStatus st invId InvCon - sendControlMessage c sq $ A_CON externalIntroId - _ -> pure () - notify CON - introMsg :: IntroId -> ConnInfo -> m () introMsg introId reInfo = do logServer "<--" c srv rId "MSG " @@ -557,16 +620,45 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do | internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash | otherwise = MsgError MsgDuplicate -- this case is not possible -connectToSendQueue :: AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> VerificationKey -> m () -connectToSendQueue c sq senderKey verifyKey = do - sendConfirmation c sq senderKey +confirmQueue :: AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> ConnInfo -> m () +confirmQueue c sq senderKey cInfo = do + sendConfirmation c sq senderKey cInfo withStore $ \st -> setSndQueueStatus st sq Confirmed - sendHello c sq verifyKey - withStore $ \st -> setSndQueueStatus st sq Active -newSendQueue :: +activateQueueInitiating :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () +activateQueueInitiating c connId sq verifyKey retryInterval = + activateQueue c connId sq verifyKey retryInterval $ notifyConnected c connId + +activateQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () -> m () +activateQueue c connId sq verifyKey retryInterval afterActivation = + getActivation c connId >>= \case + Nothing -> async runActivation >>= addActivation c connId + Just _ -> pure () + where + runActivation :: m () + runActivation = do + sendHello c sq verifyKey retryInterval + withStore $ \st -> setSndQueueStatus st sq Active + removeActivation c connId + removeVerificationKey + afterActivation + removeVerificationKey :: m () + removeVerificationKey = + let safeSignKey = C.removePublicKey $ signKey sq + in withStore $ \st -> updateSignKey st sq safeSignKey + +notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () +notifyConnected c connId = do + withStore (`getConnInvitation` connId) >>= \case + Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do + withStore $ \st -> setInvitationStatus st invId InvCon + sendControlMessage c sq $ A_CON externalIntroId + _ -> pure () + atomically $ writeTBQueue (subQ c) ("", connId, CON) + +newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) -newSendQueue (SMPQueueInfo smpServer senderId encryptKey) = do +newSndQueue (SMPQueueInfo smpServer senderId encryptKey) = do size <- asks $ rsaKeySize . config (senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size (verifyKey, signKey) <- liftIO $ C.generateKeyPair size diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 7af54fb37..b4ff3069a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -12,11 +12,12 @@ module Simplex.Messaging.Agent.Client newAgentClient, AgentMonad, getSMPServerClient, - closeSMPServerClients, - newReceiveQueue, + closeAgentClient, + newRcvQueue, subscribeQueue, addSubscription, sendConfirmation, + RetryInterval (..), sendHello, secureQueue, sendAgentMessage, @@ -28,10 +29,13 @@ module Simplex.Messaging.Agent.Client logServer, removeSubscription, cryptoError, + addActivation, + getActivation, + removeActivation, ) where -import Control.Concurrent.Async (Async) +import Control.Concurrent.Async (Async, uninterruptibleCancel) import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except @@ -66,6 +70,7 @@ data AgentClient = AgentClient smpClients :: TVar (Map SMPServer SMPClient), subscrSrvrs :: TVar (Map SMPServer (Set ConnId)), subscrConns :: TVar (Map ConnId SMPServer), + activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress clientId :: Int, agentEnv :: Env, smpSubscriber :: Async () @@ -80,8 +85,9 @@ newAgentClient agentEnv = do smpClients <- newTVar M.empty subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty + activations <- newTVar M.empty clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId, agentEnv, smpSubscriber = undefined} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, clientId, agentEnv, smpSubscriber = undefined} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) @@ -126,8 +132,16 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = notifySub :: ConnId -> IO () notifySub connId = atomically $ writeTBQueue (subQ c) ("", connId, END) -closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m () -closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient +closeAgentClient :: MonadUnliftIO m => AgentClient -> m () +closeAgentClient c = liftIO $ do + closeSMPServerClients c + cancelActivations c + +closeSMPServerClients :: AgentClient -> IO () +closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ closeSMPClient + +cancelActivations :: AgentClient -> IO () +cancelActivations c = readTVarIO (activations c) >>= mapM_ uninterruptibleCancel withSMP_ :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> m a) -> m a withSMP_ c srv action = @@ -164,8 +178,8 @@ smpClientError = \case SMPTransportError e -> BROKER $ TRANSPORT e e -> INTERNAL $ show e -newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueInfo) -newReceiveQueue c srv = do +newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueInfo) +newRcvQueue c srv = do size <- asks $ rsaKeySize . config (recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size logServer "-->" c srv "" "NEW" @@ -178,7 +192,6 @@ newReceiveQueue c srv = do rcvId, rcvPrivateKey, sndId = Just sId, - sndKey = Nothing, decryptKey, verifyKey = Nothing, status = New @@ -213,6 +226,15 @@ removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ let cs' = S.delete connId cs in if S.null cs' then Nothing else Just cs' +addActivation :: MonadUnliftIO m => AgentClient -> ConnId -> Async () -> m () +addActivation c connId a = atomically . modifyTVar (activations c) $ M.insert connId a + +getActivation :: MonadUnliftIO m => AgentClient -> ConnId -> m (Maybe (Async ())) +getActivation c connId = M.lookup connId <$> readTVarIO (activations c) + +removeActivation :: MonadUnliftIO m => AgentClient -> ConnId -> m () +removeActivation c connId = atomically . modifyTVar (activations c) $ M.delete connId + logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m () logServer dir AgentClient {clientId} srv qId cmdStr = logInfo . decodeUtf8 $ B.unwords ["A", "(" <> bshow clientId <> ")", dir, showServer srv, ":", logSecret qId, cmdStr] @@ -223,20 +245,26 @@ showServer srv = B.pack $ host srv <> maybe "" (":" <>) (port srv) logSecret :: ByteString -> ByteString logSecret bs = encode $ B.take 3 bs -sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> m () -sendConfirmation c sq@SndQueue {server, sndId} senderKey = +sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> ConnInfo -> m () +sendConfirmation c sq@SndQueue {server, sndId} senderKey cInfo = withLogSMP_ c server sndId "SEND " $ \smp -> do msg <- mkConfirmation smp liftSMP $ sendSMPMessage smp Nothing sndId msg where mkConfirmation :: SMPClient -> m MsgBody - mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey + mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey cInfo -sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> m () -sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey = +data RetryInterval = RetryInterval + { initialInterval :: Int, + increaseAfter :: Int, + maxInterval :: Int + } + +sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m () +sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey RetryInterval {initialInterval, increaseAfter, maxInterval} = withLogSMP_ c server sndId "SEND (retrying)" $ \smp -> do msg <- mkHello smp $ AckMode On - liftSMP $ send 8 100000 msg smp + liftSMP $ send 0 initialInterval msg smp where mkHello :: SMPClient -> AckMode -> m ByteString mkHello smp ackMode = do @@ -250,12 +278,15 @@ sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey = } send :: Int -> Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO () - send 0 _ _ _ = throwE $ SMPServerError AUTH - send retry delay msg smp = + send elapsedTime delay msg smp = sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case SMPServerError AUTH -> do threadDelay delay - send (retry - 1) (delay * 3 `div` 2) msg smp + let newDelay = + if elapsedTime < increaseAfter || delay == maxInterval + then delay + else min (delay * 3 `div` 2) maxInterval + send (elapsedTime + delay) newDelay msg smp e -> throwE e secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 0b81b6f9b..590ca63c5 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -46,9 +46,9 @@ module Simplex.Messaging.Agent.Protocol ATransmissionOrError, ARawTransmission, ConnId, + ConfirmationId, IntroId, InvitationId, - ReplyMode (..), AckMode (..), OnOff (..), MsgIntegrity (..), @@ -155,10 +155,13 @@ type ConnInfo = ByteString data ACommand (p :: AParty) where NEW :: ACommand Client -- response INV INV :: SMPQueueInfo -> ACommand Agent - JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client -- response OK + JOIN :: SMPQueueInfo -> ConnInfo -> ACommand Client -- response OK + CONF :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender + LET :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client INTRO :: ConnId -> ConnInfo -> ACommand Client REQ :: InvitationId -> ConnInfo -> ACommand Agent ACPT :: InvitationId -> ConnInfo -> ACommand Client + INFO :: ConnInfo -> ACommand Agent CON :: ACommand Agent -- notification that connection is established ICON :: ConnId -> ACommand Agent SUB :: ACommand Client @@ -192,7 +195,12 @@ data MsgMeta = MsgMeta data SMPMessage = -- | SMP confirmation -- (see ) - SMPConfirmation SenderPublicKey + SMPConfirmation + { -- | sender's public key to use for authentication of sender's commands at the recepient's server + senderKey :: SenderPublicKey, + -- | sender's information to be associated with the connection, e.g. sender's profile information + connInfo :: ConnInfo + } | -- | Agent message header and envelope for client messages -- (see ) SMPMessage @@ -232,12 +240,10 @@ parseSMPMessage :: ByteString -> Either AgentErrorType SMPMessage parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE where smpMessageP :: Parser SMPMessage - smpMessageP = - smpConfirmationP <* A.endOfLine - <|> A.endOfLine *> smpClientMessageP + smpMessageP = A.endOfLine *> smpClientMessageP <|> smpConfirmationP smpConfirmationP :: Parser SMPMessage - smpConfirmationP = SMPConfirmation <$> ("KEY " *> C.pubKeyP <* A.endOfLine) + smpConfirmationP = "KEY " *> (SMPConfirmation <$> C.pubKeyP <* A.endOfLine <* A.endOfLine <*> binaryBodyP <* A.endOfLine) smpClientMessageP :: Parser SMPMessage smpClientMessageP = @@ -252,7 +258,7 @@ parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE -- | Serialize SMP message. serializeSMPMessage :: SMPMessage -> ByteString serializeSMPMessage = \case - SMPConfirmation sKey -> smpMessage ("KEY " <> C.serializePubKey sKey) "" "" + SMPConfirmation sKey cInfo -> smpMessage ("KEY " <> C.serializePubKey sKey) "" (serializeBinary cInfo) <> "\n" SMPMessage {senderMsgId, senderTimestamp, previousMsgHash, agentMessage} -> let header = messageHeader senderMsgId senderTimestamp previousMsgHash body = serializeAgentMessage agentMessage @@ -274,15 +280,12 @@ agentMessageP = where hello = HELLO <$> C.pubKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP - a_msg = A_MSG <$> binaryBody - a_intro = A_INTRO <$> A.takeTill (== ' ') <* A.space <*> binaryBody + a_msg = A_MSG <$> binaryBodyP <* A.endOfLine + a_intro = A_INTRO <$> A.takeTill (== ' ') <* A.space <*> binaryBodyP <* A.endOfLine a_inv = invP A_INV a_req = invP A_REQ a_con = A_CON <$> A.takeTill wordEnd - invP f = f <$> A.takeTill (== ' ') <* A.space <*> smpQueueInfoP <* A.space <*> binaryBody - binaryBody = do - size :: Int <- A.decimal <* A.endOfLine - A.take size <* A.endOfLine + invP f = f <$> A.takeTill (== ' ') <* A.space <*> smpQueueInfoP <* A.space <*> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) -- | SMP queue information parser. @@ -302,14 +305,14 @@ serializeAgentMessage :: AMessage -> ByteString serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo - A_MSG body -> "MSG " <> serializeMsg body <> "\n" - A_INTRO introId cInfo -> "INTRO " <> introId <> " " <> serializeMsg cInfo <> "\n" + A_MSG body -> "MSG " <> serializeBinary body <> "\n" + A_INTRO introId cInfo -> "INTRO " <> introId <> " " <> serializeBinary cInfo <> "\n" A_INV introId qInfo cInfo -> "INV " <> serializeInv introId qInfo cInfo A_REQ introId qInfo cInfo -> "REQ " <> serializeInv introId qInfo cInfo A_CON introId -> "CON " <> introId where serializeInv introId qInfo cInfo = - B.intercalate " " [introId, serializeSmpQueueInfo qInfo, serializeMsg cInfo] <> "\n" + B.intercalate " " [introId, serializeSmpQueueInfo qInfo, serializeBinary cInfo] <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString @@ -335,6 +338,8 @@ instance IsString SMPServer where -- | SMP agent connection alias. type ConnId = ByteString +type ConfirmationId = ByteString + type IntroId = ByteString type InvitationId = ByteString @@ -351,9 +356,6 @@ newtype AckMode = AckMode OnOff deriving (Eq, Show) data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey deriving (Eq, Show) --- | Connection reply mode (used in JOIN command). -newtype ReplyMode = ReplyMode OnOff deriving (Eq, Show) - -- | Public key used to E2E encrypt SMP messages. type EncryptionKey = C.PublicKey @@ -361,7 +363,7 @@ type EncryptionKey = C.PublicKey type DecryptionKey = C.SafePrivateKey -- | Private key used to sign SMP commands -type SignatureKey = C.SafePrivateKey +type SignatureKey = C.APrivateKey -- | Public key used by SMP server to authorize (verify) SMP commands. type VerificationKey = C.PublicKey @@ -476,9 +478,12 @@ commandP = "NEW" $> ACmd SClient NEW <|> "INV " *> invResp <|> "JOIN " *> joinCmd + <|> "CONF " *> confCmd + <|> "LET " *> letCmd <|> "INTRO " *> introCmd <|> "REQ " *> reqCmd <|> "ACPT " *> acptCmd + <|> "INFO " *> infoCmd <|> "SUB" $> ACmd SClient SUB <|> "END" $> ACmd SAgent END <|> "SEND " *> sendCmd @@ -492,10 +497,13 @@ commandP = <|> "OK" $> ACmd SAgent OK where invResp = ACmd SAgent . INV <$> smpQueueInfoP - joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <*> replyMode) + joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <* A.space <*> A.takeByteString) + confCmd = ACmd SAgent <$> (CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) introCmd = ACmd SClient <$> introP INTRO reqCmd = ACmd SAgent <$> introP REQ acptCmd = ACmd SClient <$> introP ACPT + infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString sentResp = ACmd SAgent . SENT <$> A.decimal iconMsg = ACmd SAgent . ICON <$> A.takeTill wordEnd @@ -507,7 +515,6 @@ commandP = sender <- " S=" *> partyMeta A.decimal pure MsgMeta {integrity, recipient, broker, sender} introP f = f <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString - replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On) partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P agentError = ACmd SAgent . ERR <$> agentErrorTypeP @@ -529,16 +536,19 @@ serializeCommand :: ACommand p -> ByteString serializeCommand = \case NEW -> "NEW" INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo - JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode - INTRO connId cInfo -> "INTRO " <> connId <> " " <> serializeMsg cInfo - REQ invId cInfo -> "REQ " <> invId <> " " <> serializeMsg cInfo - ACPT invId cInfo -> "ACPT " <> invId <> " " <> serializeMsg cInfo + JOIN qInfo cInfo -> "JOIN " <> serializeSmpQueueInfo qInfo <> " " <> serializeBinary cInfo + CONF confId cInfo -> "CONF " <> confId <> " " <> serializeBinary cInfo + LET confId cInfo -> "LET " <> confId <> " " <> serializeBinary cInfo + INTRO connId cInfo -> "INTRO " <> connId <> " " <> serializeBinary cInfo + REQ invId cInfo -> "REQ " <> invId <> " " <> serializeBinary cInfo + ACPT invId cInfo -> "ACPT " <> invId <> " " <> serializeBinary cInfo + INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" - SEND msgBody -> "SEND " <> serializeMsg msgBody + SEND msgBody -> "SEND " <> serializeBinary msgBody SENT mId -> "SENT " <> bshow mId MSG msgMeta msgBody -> - "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeMsg msgBody + "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeBinary msgBody OFF -> "OFF" DEL -> "DEL" CON -> "CON" @@ -546,10 +556,6 @@ serializeCommand = \case ERR e -> "ERR " <> serializeAgentError e OK -> "OK" where - replyMode :: ReplyMode -> ByteString - replyMode = \case - ReplyMode Off -> " NO_REPLY" - ReplyMode On -> "" showTs :: UTCTime -> ByteString showTs = B.pack . formatISO8601Millis serializeMsgMeta :: MsgMeta -> ByteString @@ -590,8 +596,13 @@ serializeAgentError = \case BROKER (TRANSPORT e) -> "BROKER TRANSPORT " <> serializeTransportError e e -> bshow e -serializeMsg :: ByteString -> ByteString -serializeMsg body = bshow (B.length body) <> "\n" <> body +binaryBodyP :: Parser ByteString +binaryBodyP = do + size :: Int <- A.decimal <* A.endOfLine + A.take size + +serializeBinary :: ByteString -> ByteString +serializeBinary body = bshow (B.length body) <> "\n" <> body -- | Send raw (unparsed) SMP agent protocol transmission to TCP connection. tPutRaw :: Transport c => c -> ARawTransmission -> IO () @@ -639,17 +650,21 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p)) cmdWithMsgBody = \case - SEND body -> SEND <$$> getMsgBody body - MSG msgMeta body -> MSG msgMeta <$$> getMsgBody body - INTRO introId cInfo -> INTRO introId <$$> getMsgBody cInfo - REQ introId cInfo -> REQ introId <$$> getMsgBody cInfo - ACPT introId cInfo -> ACPT introId <$$> getMsgBody cInfo + SEND body -> SEND <$$> getBody body + MSG msgMeta body -> MSG msgMeta <$$> getBody body + INTRO introId cInfo -> INTRO introId <$$> getBody cInfo + REQ introId cInfo -> REQ introId <$$> getBody cInfo + ACPT introId cInfo -> ACPT introId <$$> getBody cInfo + JOIN qInfo cInfo -> JOIN qInfo <$$> getBody cInfo + CONF confId cInfo -> CONF confId <$$> getBody cInfo + LET confId cInfo -> LET confId <$$> getBody cInfo + INFO cInfo -> INFO <$$> getBody cInfo cmd -> pure $ Right cmd -- TODO refactor with server - getMsgBody :: MsgBody -> m (Either AgentErrorType MsgBody) - getMsgBody msgBody = - case B.unpack msgBody of + getBody :: ByteString -> m (Either AgentErrorType ByteString) + getBody binary = + case B.unpack binary of ':' : body -> return . Right $ B.pack body str -> case readMaybe str :: Maybe Int of Just size -> liftIO $ do diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index cad975570..6b49ad280 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -46,14 +46,19 @@ class Monad m => MonadAgentStore s m where setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m () setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m () setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m () + updateSignKey :: s -> SndQueue -> SignatureKey -> m () + + -- Confirmations + createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId + acceptConfirmation :: s -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation + getAcceptedConfirmation :: s -> ConnId -> m AcceptedConfirmation + removeConfirmations :: s -> ConnId -> m () -- Msg management updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) createRcvMsg :: s -> ConnId -> RcvMsgData -> m () - updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) createSndMsg :: s -> ConnId -> SndMsgData -> m () - getMsg :: s -> ConnId -> InternalId -> m Msg -- Introductions @@ -76,7 +81,6 @@ data RcvQueue = RcvQueue rcvId :: SMP.RecipientId, rcvPrivateKey :: RecipientPrivateKey, sndId :: Maybe SMP.SenderId, - sndKey :: Maybe SenderPublicKey, decryptKey :: DecryptionKey, verifyKey :: Maybe VerificationKey, status :: QueueStatus @@ -152,6 +156,22 @@ deriving instance Show SomeConn data ConnData = ConnData {connId :: ConnId, viaInv :: Maybe InvitationId, connLevel :: Int} deriving (Eq, Show) +-- * Confirmation types + +data NewConfirmation = NewConfirmation + { connId :: ConnId, + senderKey :: SenderPublicKey, + senderConnInfo :: ConnInfo + } + +data AcceptedConfirmation = AcceptedConfirmation + { confirmationId :: ConfirmationId, + connId :: ConnId, + senderKey :: SenderPublicKey, + senderConnInfo :: ConnInfo, + ownConnInfo :: ConnInfo + } + -- * Message integrity validation types type MsgHash = ByteString @@ -372,6 +392,8 @@ data StoreError | -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa. -- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error. SEBadConnType ConnType + | -- | Confirmation not found. + SEConfirmationNotFound | -- | Introduction ID not found. SEIntroNotFound | -- | Invitation ID not found. diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 3d3ec8d67..38ec4c23b 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -184,7 +184,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto getAllConnIds :: SQLiteStore -> m [ConnId] getAllConnIds st = - liftIO . withConnection st $ \db -> do + liftIO . withConnection st $ \db -> concat <$> (DB.query_ db "SELECT conn_alias FROM connections;" :: IO [[ConnId]]) getRcvConn :: SQLiteStore -> SMPServer -> SMP.RecipientId -> m SomeConn @@ -278,6 +278,86 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] + updateSignKey :: SQLiteStore -> SndQueue -> SignatureKey -> m () + updateSignKey st SndQueue {sndId, server = SMPServer {host, port}} signatureKey = + liftIO . withConnection st $ \db -> + DB.executeNamed + db + [sql| + UPDATE snd_queues + SET sign_key = :sign_key + WHERE host = :host AND port = :port AND snd_id = :snd_id; + |] + [":sign_key" := signatureKey, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] + + createConfirmation :: SQLiteStore -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId + createConfirmation st gVar NewConfirmation {connId, senderKey, senderConnInfo} = + liftIOEither . withTransaction st $ \db -> + createWithRandomId gVar $ \confirmationId -> + DB.execute + db + [sql| + INSERT INTO conn_confirmations + (confirmation_id, conn_alias, sender_key, sender_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + |] + (confirmationId, connId, senderKey, senderConnInfo) + + acceptConfirmation :: SQLiteStore -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation + acceptConfirmation st confirmationId ownConnInfo = + liftIOEither . withTransaction st $ \db -> do + DB.executeNamed + db + [sql| + UPDATE conn_confirmations + SET accepted = 1, + own_conn_info = :own_conn_info + WHERE confirmation_id = :confirmation_id; + |] + [ ":own_conn_info" := ownConnInfo, + ":confirmation_id" := confirmationId + ] + confirmation + <$> DB.query + db + [sql| + SELECT conn_alias, sender_key, sender_conn_info + FROM conn_confirmations + WHERE confirmation_id = ?; + |] + (Only confirmationId) + where + confirmation [(connId, senderKey, senderConnInfo)] = + Right $ AcceptedConfirmation {confirmationId, connId, senderKey, senderConnInfo, ownConnInfo} + confirmation _ = Left SEConfirmationNotFound + + getAcceptedConfirmation :: SQLiteStore -> ConnId -> m AcceptedConfirmation + getAcceptedConfirmation st connId = + liftIOEither . withConnection st $ \db -> + confirmation + <$> DB.query + db + [sql| + SELECT confirmation_id, sender_key, sender_conn_info, own_conn_info + FROM conn_confirmations + WHERE conn_alias = ? AND accepted = 1; + |] + (Only connId) + where + confirmation [(confirmationId, senderKey, senderConnInfo, ownConnInfo)] = + Right $ AcceptedConfirmation {confirmationId, connId, senderKey, senderConnInfo, ownConnInfo} + confirmation _ = Left SEConfirmationNotFound + + removeConfirmations :: SQLiteStore -> ConnId -> m () + removeConfirmations st connId = + liftIO . withConnection st $ \db -> + DB.executeNamed + db + [sql| + DELETE FROM conn_confirmations + WHERE conn_alias = :conn_alias; + |] + [":conn_alias" := connId] + updateRcvIds :: SQLiteStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) updateRcvIds st connId = liftIO . withTransaction st $ \db -> do @@ -548,9 +628,9 @@ insertRcvQueue_ dbConn connId RcvQueue {..} = do dbConn [sql| INSERT INTO rcv_queues - ( host, port, rcv_id, conn_alias, rcv_private_key, snd_id, snd_key, decrypt_key, verify_key, status) + ( host, port, rcv_id, conn_alias, rcv_private_key, snd_id, decrypt_key, verify_key, status) VALUES - (:host,:port,:rcv_id,:conn_alias,:rcv_private_key,:snd_id,:snd_key,:decrypt_key,:verify_key,:status); + (:host,:port,:rcv_id,:conn_alias,:rcv_private_key,:snd_id,:decrypt_key,:verify_key,:status); |] [ ":host" := host server, ":port" := port_, @@ -558,7 +638,6 @@ insertRcvQueue_ dbConn connId RcvQueue {..} = do ":conn_alias" := connId, ":rcv_private_key" := rcvPrivateKey, ":snd_id" := sndId, - ":snd_key" := sndKey, ":decrypt_key" := decryptKey, ":verify_key" := verifyKey, ":status" := status @@ -655,16 +734,16 @@ getRcvQueueByConnAlias_ dbConn connId = dbConn [sql| SELECT s.key_hash, q.host, q.port, q.rcv_id, q.rcv_private_key, - q.snd_id, q.snd_key, q.decrypt_key, q.verify_key, q.status + q.snd_id, q.decrypt_key, q.verify_key, q.status FROM rcv_queues q INNER JOIN servers s ON q.host = s.host AND q.port = s.port WHERE q.conn_alias = ?; |] (Only connId) where - rcvQueue [(keyHash, host, port, rcvId, rcvPrivateKey, sndId, sndKey, decryptKey, verifyKey, status)] = + rcvQueue [(keyHash, host, port, rcvId, rcvPrivateKey, sndId, decryptKey, verifyKey, status)] = let srv = SMPServer host (deserializePort_ port) keyHash - in Just $ RcvQueue srv rcvId rcvPrivateKey sndId sndKey decryptKey verifyKey status + in Just $ RcvQueue srv rcvId rcvPrivateKey sndId decryptKey verifyKey status rcvQueue _ = Nothing getSndQueueByConnAlias_ :: DB.Connection -> ConnId -> IO (Maybe SndQueue) diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index c56161712..bc709e377 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -20,18 +20,20 @@ -- . module Simplex.Messaging.Crypto ( -- * RSA keys - PrivateKey (rsaPrivateKey), - SafePrivateKey, -- constructor is not exported + PrivateKey (rsaPrivateKey, publicKey), + SafePrivateKey (..), -- constructor is not exported FullPrivateKey (..), + APrivateKey (..), PublicKey (..), SafeKeyPair, FullKeyPair, KeyHash (..), generateKeyPair, - publicKey, + publicKey', publicKeySize, validKeySize, safePrivateKey, + removePublicKey, -- * E2E hybrid encryption scheme encrypt, @@ -121,6 +123,9 @@ newtype SafePrivateKey = SafePrivateKey {unPrivateKey :: R.PrivateKey} deriving -- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (with PublicKey inside). newtype FullPrivateKey = FullPrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show) +-- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (PublicKey may be inside). +newtype APrivateKey = APrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show) + -- | Type-class used for both private key types: SafePrivateKey and FullPrivateKey. class PrivateKey k where -- unwraps 'Crypto.PubKey.RSA.PrivateKey' @@ -132,16 +137,36 @@ class PrivateKey k where -- smart constructor removing public key from SafePrivateKey but keeping it in FullPrivateKey mkPrivateKey :: R.PrivateKey -> k + -- extracts public key from private key + publicKey :: k -> Maybe PublicKey + +-- | Remove public key exponent from APrivateKey. +removePublicKey :: APrivateKey -> APrivateKey +removePublicKey (APrivateKey R.PrivateKey {private_pub = k, private_d}) = + APrivateKey $ unPrivateKey (safePrivateKey (R.public_size k, R.public_n k, private_d) :: SafePrivateKey) + instance PrivateKey SafePrivateKey where rsaPrivateKey = unPrivateKey _privateKey = SafePrivateKey mkPrivateKey R.PrivateKey {private_pub = k, private_d} = safePrivateKey (R.public_size k, R.public_n k, private_d) + publicKey _ = Nothing instance PrivateKey FullPrivateKey where rsaPrivateKey = unPrivateKey _privateKey = FullPrivateKey mkPrivateKey = FullPrivateKey + publicKey = Just . PublicKey . R.private_pub . rsaPrivateKey + +instance PrivateKey APrivateKey where + rsaPrivateKey = unPrivateKey + _privateKey = APrivateKey + mkPrivateKey = APrivateKey + publicKey pk = + let k = R.private_pub $ rsaPrivateKey pk + in if R.public_e k == 0 + then Nothing + else Just $ PublicKey k instance IsString FullPrivateKey where fromString = parseString (decode >=> decodePrivKey) @@ -151,10 +176,14 @@ instance IsString PublicKey where instance ToField SafePrivateKey where toField = toField . encodePrivKey +instance ToField APrivateKey where toField = toField . encodePrivKey + instance ToField PublicKey where toField = toField . encodePubKey instance FromField SafePrivateKey where fromField = blobFieldParser binaryPrivKeyP +instance FromField APrivateKey where fromField = blobFieldParser binaryPrivKeyP + instance FromField PublicKey where fromField = blobFieldParser binaryPubKeyP -- | Tuple of RSA 'PublicKey' and 'PrivateKey'. @@ -217,8 +246,8 @@ generateKeyPair size = loop privateKeySize :: PrivateKey k => k -> Int privateKeySize = R.public_size . R.private_pub . rsaPrivateKey -publicKey :: FullPrivateKey -> PublicKey -publicKey = PublicKey . R.private_pub . rsaPrivateKey +publicKey' :: FullPrivateKey -> PublicKey +publicKey' = PublicKey . R.private_pub . rsaPrivateKey publicKeySize :: PublicKey -> Int publicKeySize = R.public_size . rsaPublicKey diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 61873af27..83282f03f 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -86,7 +86,7 @@ newEnv config = do idsDrg <- drgNew >>= newTVarIO s' <- restoreQueues queueStore `mapM` storeLog (config :: ServerConfig) let pk = serverPrivateKey config - serverKeyPair = (C.publicKey pk, pk) + serverKeyPair = (C.publicKey' pk, pk) return Env {config, server, queueStore, msgStore, idsDrg, serverKeyPair, storeLog = s'} where restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 576c2ea63..2f0866de2 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -107,11 +107,14 @@ testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do ("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "alice", "JOIN " <> qInfo') #> ("11", "alice", OK) + bob #: ("11", "alice", "JOIN " <> qInfo' <> " 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") bob <# ("", "alice", CON) alice <# ("", "bob", CON) - alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", SENT 1) - alice #: ("3", "bob", "SEND :how are you?") #> ("3", "bob", SENT 2) + alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", SENT 1) + alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", SENT 2) bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", SENT 3) @@ -129,8 +132,11 @@ testAgentClient = do bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do (bobId, qInfo) <- createConnection alice Nothing - aliceId <- joinConnection bob Nothing qInfo + aliceId <- joinConnection bob Nothing qInfo "bob's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice + allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) + get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) InternalId 1 <- sendMessage alice bobId "hello" InternalId 2 <- sendMessage alice bobId "how are you?" @@ -163,7 +169,11 @@ testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> qInfo') + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> qInfo' <> " 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 + bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, SENT 1) @@ -209,11 +219,12 @@ testIntroduction _ alice bob tom = do ("", "alice", Right (REQ invId1 "meet tom")) <- (bob <#:) bob #: ("2", "tom_via_alice", "ACPT " <> invId1 <> " 7\nI'm bob") #> ("2", "tom_via_alice", OK) ("", "alice", Right (REQ invId2 "I'm bob")) <- (tom <#:) - -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter - -- or this should be another command, not ACPT tom #: ("3", "bob_via_alice", "ACPT " <> invId2 <> " 8\ntom here") #> ("3", "bob_via_alice", OK) - tom <# ("", "bob_via_alice", CON) + ("", "tom_via_alice", Right (CONF confId "tom here")) <- (bob <#:) + bob #: ("3.1", "tom_via_alice", "LET " <> confId <> " 7\nI'm bob") #> ("3.1", "tom_via_alice", OK) bob <# ("", "tom_via_alice", CON) + tom <# ("", "bob_via_alice", INFO "I'm bob") + tom <# ("", "bob_via_alice", CON) alice <# ("", "bob", ICON "tom") -- they can message each other now tom #: ("4", "bob_via_alice", "SEND :hello") #> ("4", "bob_via_alice", SENT 1) @@ -230,14 +241,16 @@ testIntroductionRandomIds _ alice bob tom = do alice #: ("1", bobA, "INTRO " <> tomA <> " 8\nmeet tom") #> ("1", bobA, OK) ("", aliceB', Right (REQ invId1 "meet tom")) <- (bob <#:) aliceB' `shouldBe` aliceB - ("2", tomB, Right OK) <- bob #: ("2", "C:", "ACPT " <> invId1 <> " 7\nI'm bob") + ("2", tomB, Right OK) <- bob #: ("2", "", "ACPT " <> invId1 <> " 7\nI'm bob") ("", aliceT', Right (REQ invId2 "I'm bob")) <- (tom <#:) aliceT' `shouldBe` aliceT - -- TODO info "tom here" is not used, either JOIN command also should have eInfo parameter - -- or this should be another command, not ACPT ("3", bobT, Right OK) <- tom #: ("3", "", "ACPT " <> invId2 <> " 8\ntom here") - tom <# ("", bobT, CON) + ("", tomB', Right (CONF confId "tom here")) <- (bob <#:) + tomB' `shouldBe` tomB + bob #: ("3.1", tomB, "LET " <> confId <> " 7\nI'm bob") =#> \case ("3.1", c, OK) -> c == tomB; _ -> False bob <# ("", tomB, CON) + tom <# ("", bobT, INFO "I'm bob") + tom <# ("", bobT, CON) alice <# ("", bobA, ICON tomA) -- they can message each other now tom #: ("4", bobT, "SEND :hello") #> ("4", bobT, SENT 1) @@ -249,7 +262,10 @@ connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") let qInfo' = serializeSmpQueueInfo qInfo - h2 #: ("c2", name1, "JOIN " <> qInfo') #> ("c2", name1, OK) + h2 #: ("c2", name1, "JOIN " <> qInfo' <> " 5\ninfo2") #> ("c2", name1, OK) + ("", _, Right (CONF connId "info2")) <- (h1 <#:) + h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) + h2 <# ("", name1, INFO "info1") h2 <# ("", name1, CON) h1 <# ("", name2, CON) @@ -257,7 +273,10 @@ connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) connect' h1 h2 = do ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo - ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo') + ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo' <> " 5\ninfo2") + ("", _, Right (CONF connId "info2")) <- (h1 <#:) + h1 #: ("c3", conn2, "LET " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False + h2 <# ("", conn1, INFO "info1") h2 <# ("", conn1, CON) h1 <# ("", conn2, CON) pure (conn1, conn2) @@ -281,7 +300,7 @@ syntaxTests t = do -- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided) -- TODO: add tests with defined connection alias it "using same server as in invitation" $ - ("311", "a", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "a", "ERR SMP AUTH") + ("311", "a", "JOIN smp::localhost:5000::1234::" <> samplePublicKey <> " 14\nbob's connInfo") >#> ("311", "a", "ERR SMP AUTH") describe "invalid" do -- TODO: JOIN is not merged yet - to be added it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 56fd88514..0f364c969 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -156,7 +156,6 @@ rcvQueue1 = rcvId = "1234", rcvPrivateKey = C.safePrivateKey (1, 2, 3), sndId = Just "2345", - sndKey = Nothing, decryptKey = C.safePrivateKey (1, 2, 3), verifyKey = Nothing, status = New @@ -169,7 +168,7 @@ sndQueue1 = sndId = "3456", sndPrivateKey = C.safePrivateKey (1, 2, 3), encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = C.safePrivateKey (1, 2, 3), + signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), status = New } @@ -309,7 +308,7 @@ testUpgradeRcvConnToDuplex = sndId = "2345", sndPrivateKey = C.safePrivateKey (1, 2, 3), encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = C.safePrivateKey (1, 2, 3), + signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), status = New } upgradeRcvConnToDuplex store "conn1" anotherSndQueue @@ -329,7 +328,6 @@ testUpgradeSndConnToDuplex = rcvId = "3456", rcvPrivateKey = C.safePrivateKey (1, 2, 3), sndId = Just "4567", - sndKey = Nothing, decryptKey = C.safePrivateKey (1, 2, 3), verifyKey = Nothing, status = New From 8ba3e3e45a6006d173738db9eac1068edad74df7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 3 Jul 2021 20:18:37 +0100 Subject: [PATCH 09/29] remove connection ID parameter from agent functions (#166) * remove connection ID parameter from agent functions * remove unused extension --- src/Simplex/Messaging/Agent.hs | 19 +++++++++---------- src/Simplex/Messaging/Agent/Env/SQLite.hs | 6 +++--- src/Simplex/Messaging/Agent/Store/SQLite.hs | 2 ++ tests/AgentTests.hs | 5 ++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 2581a9dc0..06dbbfc39 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -72,7 +72,6 @@ import Data.Composition ((.:), (.:.)) import Data.Functor (($>)) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8) import Data.Time.Clock @@ -132,20 +131,20 @@ disconnectAgentClient c = closeAgentClient c >> logConnection c False type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) -- | Create SMP agent connection (NEW command) in Reader monad -createConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> m (ConnId, SMPQueueInfo) -createConnection' c connId = newConn c (fromMaybe "" connId) Nothing 0 +createConnection' :: AgentMonad m => AgentClient -> m (ConnId, SMPQueueInfo) +createConnection' c = newConn c "" Nothing 0 -- | Create SMP agent connection (NEW command) -createConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> m (ConnId, SMPQueueInfo) -createConnection c = (`runReaderT` agentEnv c) . createConnection' c +createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) +createConnection c = (`runReaderT` agentEnv c) $ createConnection' c -- | Join SMP agent connection (JOIN command) in Reader monad -joinConnection' :: AgentMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection' c connId qInfo cInfo = joinConn c (fromMaybe "" connId) qInfo cInfo Nothing 0 +joinConnection' :: AgentMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConnection' c qInfo cInfo = joinConn c "" qInfo cInfo Nothing 0 -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> Maybe ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection c = (`runReaderT` agentEnv c) .:. joinConnection' c +joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConnection c = (`runReaderT` agentEnv c) .: joinConnection' c -- | Approve confirmation (LET command) allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () @@ -230,7 +229,7 @@ withStore :: (forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) -> m a withStore action = do - st <- asks store' + st <- asks store runExceptT (action st `E.catch` handleInternal) >>= \case Right c -> return c Left e -> throwError $ storeError e diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 6d1fa30ba..5fbe69739 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -30,7 +30,7 @@ data AgentConfig = AgentConfig data Env = Env { config :: AgentConfig, - store' :: SQLiteStore, + store :: SQLiteStore, idsDrg :: TVar ChaChaDRG, clientCounter :: TVar Int, reservedMsgSize :: Int, @@ -40,10 +40,10 @@ data Env = Env newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env newSMPAgentEnv cfg = do idsDrg <- newTVarIO =<< drgNew - store' <- liftIO $ createSQLiteStore (dbFile cfg) (dbPoolSize cfg) Migrations.app + store <- liftIO $ createSQLiteStore (dbFile cfg) (dbPoolSize cfg) Migrations.app clientCounter <- newTVarIO 0 randomServer <- newTVarIO =<< liftIO newStdGen - return Env {config = cfg, store', idsDrg, clientCounter, reservedMsgSize, randomServer} + return Env {config = cfg, store, idsDrg, clientCounter, reservedMsgSize, randomServer} where -- 1st rsaKeySize is used by the RSA signature in each command, -- 2nd - by encrypted message body header diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 38ec4c23b..ca6a088fa 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -21,6 +21,8 @@ module Simplex.Messaging.Agent.Store.SQLite createSQLiteStore, connectSQLiteStore, withConnection, + withTransaction, + fromTextField_, ) where diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 2f0866de2..f48816007 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -2,7 +2,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} @@ -131,8 +130,8 @@ testAgentClient = do alice <- getSMPAgentClient cfg bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice Nothing - aliceId <- joinConnection bob Nothing qInfo "bob's connInfo" + (bobId, qInfo) <- createConnection alice + aliceId <- joinConnection bob qInfo "bob's connInfo" ("", _, CONF confId "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) From 5e380e1d478d7e37b56fd285ca4a77043a8275a1 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sat, 10 Jul 2021 20:02:48 +1000 Subject: [PATCH 10/29] add tests for establishing connection asynchronously; retrieve correct key for activation on restart (#169) --- src/Simplex/Messaging/Agent.hs | 20 ++-- tests/AgentTests.hs | 53 +-------- tests/AgentTests/FunctionalAPITests.hs | 151 +++++++++++++++++++++++++ tests/Test.hs | 2 +- 4 files changed, 166 insertions(+), 60 deletions(-) create mode 100644 tests/AgentTests/FunctionalAPITests.hs diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 06dbbfc39..53599d709 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -38,6 +38,7 @@ module Simplex.Messaging.Agent AgentMonad, AgentErrorMonad, getSMPAgentClient, + disconnectAgentClient, -- used in tests createConnection, joinConnection, allowConnection, @@ -359,27 +360,28 @@ subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq sq) -> case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \sndKey -> do - secureQueue c rq sndKey + Confirmed -> withVerifyKey sq $ \verifyKey -> do + conf <- withStore (`getAcceptedConfirmation` connId) + secureQueue c rq $ senderKey (conf :: AcceptedConfirmation) withStore $ \st -> setRcvQueueStatus st rq Secured - activateSecuredQueue rq sq sndKey + activateSecuredQueue rq sq verifyKey Secured -> withVerifyKey sq $ activateSecuredQueue rq sq Active -> subscribeQueue c rq connId _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (SndConnection _ sq) -> case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \sndKey -> - activateQueueJoining c connId sq sndKey resumeInterval + Confirmed -> withVerifyKey sq $ \verifyKey -> + activateQueueJoining c connId sq verifyKey resumeInterval Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId where withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m () withVerifyKey sq action = - let err = throwError $ INTERNAL "missing send queue public key" - in maybe err action . C.publicKey $ sndPrivateKey sq + let err = throwError $ INTERNAL "missing signing key public counterpart" + in maybe err action . C.publicKey $ signKey sq activateSecuredQueue :: RcvQueue -> SndQueue -> C.PublicKey -> m () - activateSecuredQueue rq sq sndKey = do - activateQueueInitiating c connId sq sndKey resumeInterval + activateSecuredQueue rq sq verifyKey = do + activateQueueInitiating c connId sq verifyKey resumeInterval subscribeQueue c rq connId -- | Send message to the connection (SEND command) in Reader monad diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index f48816007..e8faf5ea9 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -5,32 +5,26 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} -module AgentTests where +module AgentTests (agentTests) where +import AgentTests.FunctionalAPITests (functionalAPITests) import AgentTests.SQLiteTests (storeTests) import Control.Concurrent -import Control.Monad.Except (catchError, runExceptT) -import Control.Monad.IO.Unlift import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient -import SMPClient (withSmpServer) -import Simplex.Messaging.Agent -import Simplex.Messaging.Agent.Env.SQLite (dbFile) import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store (InternalId (..)) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) import System.Timeout import Test.Hspec -import UnliftIO.STM agentTests :: ATransport -> Spec agentTests (ATransport t) = do + describe "Functional API" $ functionalAPITests (ATransport t) describe "SQLite store" storeTests describe "SMP agent protocol syntax" $ syntaxTests t describe "Establishing duplex connection" do @@ -46,8 +40,6 @@ agentTests (ATransport t) = do smpAgentTest2_2_2 $ testDuplexConnection t it "should connect via 2 servers and 2 agents (random IDs)" $ smpAgentTest2_2_2 $ testDuplexConnRandomIds t - it "should connect via one server using SMP agent clients" $ - withSmpServer (ATransport t) testAgentClient describe "Connection subscriptions" do it "should connect via one server and one agent" $ smpAgentTest3_1_1 $ testSubscription t @@ -125,45 +117,6 @@ testDuplexConnection _ alice bob = do alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" -testAgentClient :: IO () -testAgentClient = do - alice <- getSMPAgentClient cfg - bob <- getSMPAgentClient cfg {dbFile = testDB2} - Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice - aliceId <- joinConnection bob qInfo "bob's connInfo" - ("", _, CONF confId "bob's connInfo") <- get alice - allowConnection alice bobId confId "alice's connInfo" - get alice ##> ("", bobId, CON) - get bob ##> ("", aliceId, INFO "alice's connInfo") - get bob ##> ("", aliceId, CON) - InternalId 1 <- sendMessage alice bobId "hello" - InternalId 2 <- sendMessage alice bobId "how are you?" - get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False - get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False - InternalId 3 <- sendMessage bob aliceId "hello too" - InternalId 4 <- sendMessage bob aliceId "message 1" - get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False - get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False - suspendConnection alice bobId - InternalId 0 <- sendMessage bob aliceId "message 2" `catchError` \(SMP AUTH) -> pure $ InternalId 0 - deleteConnection alice bobId - liftIO $ noMessages alice "nothing else should be delivered to alice" - pure () - where - (##>) :: MonadIO m => m (ATransmission 'Agent) -> ATransmission 'Agent -> m () - a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t) - (=##>) :: MonadIO m => m (ATransmission 'Agent) -> (ATransmission 'Agent -> Bool) -> m () - a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p) - noMessages :: AgentClient -> String -> Expectation - noMessages c err = tryGet `shouldReturn` () - where - tryGet = - 10000 `timeout` get c >>= \case - Just _ -> error err - _ -> return () - get c = atomically (readTBQueue $ subQ c) - testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs new file mode 100644 index 000000000..e2a25926e --- /dev/null +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} + +module AgentTests.FunctionalAPITests (functionalAPITests) where + +import Control.Monad.Except (ExceptT, catchError, runExceptT) +import Control.Monad.IO.Unlift +import SMPAgentClient +import SMPClient (withSmpServer) +import Simplex.Messaging.Agent +import Simplex.Messaging.Agent.Env.SQLite (dbFile) +import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store (InternalId (..)) +import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) +import Simplex.Messaging.Transport (ATransport (..)) +import System.Timeout +import Test.Hspec +import UnliftIO.STM + +(##>) :: MonadIO m => m (ATransmission 'Agent) -> ATransmission 'Agent -> m () +a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t) + +(=##>) :: MonadIO m => m (ATransmission 'Agent) -> (ATransmission 'Agent -> Bool) -> m () +a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p) + +get :: MonadIO m => AgentClient -> m (ATransmission 'Agent) +get c = atomically (readTBQueue $ subQ c) + +pattern Msg :: MsgBody -> ACommand 'Agent +pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody + +functionalAPITests :: ATransport -> Spec +functionalAPITests t = do + describe "Establishing duplex connection" $ + it "should connect via one server using SMP agent clients" $ + withSmpServer t testAgentClient + describe "Establishing connection asynchronously" $ do + it "should connect with initiating client going offline" $ + withSmpServer t testAsyncInitiatingOffline + it "should connect with joining client going offline before its queue activation" $ + withSmpServer t testAsyncJoiningOfflineBeforeActivation + -- TODO a valid test case but not trivial to implement, probably requires some agent rework + xit "should connect with joining client going offline after its queue activation" $ + withSmpServer t testAsyncJoiningOfflineAfterActivation + it "should connect with both clients going offline" $ + withSmpServer t testAsyncBothOffline + +testAgentClient :: IO () +testAgentClient = do + alice <- getSMPAgentClient cfg + bob <- getSMPAgentClient cfg {dbFile = testDB2} + Right () <- runExceptT $ do + (bobId, qInfo) <- createConnection alice + aliceId <- joinConnection bob qInfo "bob's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice + allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, CON) + get bob ##> ("", aliceId, INFO "alice's connInfo") + get bob ##> ("", aliceId, CON) + InternalId 1 <- sendMessage alice bobId "hello" + InternalId 2 <- sendMessage alice bobId "how are you?" + get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False + InternalId 3 <- sendMessage bob aliceId "hello too" + InternalId 4 <- sendMessage bob aliceId "message 1" + get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False + suspendConnection alice bobId + InternalId 0 <- sendMessage bob aliceId "message 2" `catchError` \(SMP AUTH) -> pure $ InternalId 0 + deleteConnection alice bobId + liftIO $ noMessages alice "nothing else should be delivered to alice" + pure () + where + noMessages :: AgentClient -> String -> Expectation + noMessages c err = tryGet `shouldReturn` () + where + tryGet = + 10000 `timeout` get c >>= \case + Just _ -> error err + _ -> return () + +testAsyncInitiatingOffline :: IO () +testAsyncInitiatingOffline = do + alice <- getSMPAgentClient cfg + bob <- getSMPAgentClient cfg {dbFile = testDB2} + Right () <- runExceptT $ do + (bobId, qInfo) <- createConnection alice + disconnectAgentClient alice + aliceId <- joinConnection bob qInfo "bob's connInfo" + alice' <- liftIO $ getSMPAgentClient cfg + subscribeConnection alice' bobId + ("", _, CONF confId "bob's connInfo") <- get alice' + allowConnection alice' bobId confId "alice's connInfo" + get alice' ##> ("", bobId, CON) + get bob ##> ("", aliceId, INFO "alice's connInfo") + get bob ##> ("", aliceId, CON) + exchangeGreetings alice' bobId bob aliceId + pure () + +testAsyncJoiningOfflineBeforeActivation :: IO () +testAsyncJoiningOfflineBeforeActivation = do + alice <- getSMPAgentClient cfg + bob <- getSMPAgentClient cfg {dbFile = testDB2} + Right () <- runExceptT $ do + (bobId, qInfo) <- createConnection alice + aliceId <- joinConnection bob qInfo "bob's connInfo" + disconnectAgentClient bob + ("", _, CONF confId "bob's connInfo") <- get alice + allowConnection alice bobId confId "alice's connInfo" + bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} + subscribeConnection bob' aliceId + get alice ##> ("", bobId, CON) + get bob' ##> ("", aliceId, INFO "alice's connInfo") + get bob' ##> ("", aliceId, CON) + exchangeGreetings alice bobId bob' aliceId + pure () + +testAsyncJoiningOfflineAfterActivation :: IO () +testAsyncJoiningOfflineAfterActivation = error "not implemented" + +testAsyncBothOffline :: IO () +testAsyncBothOffline = do + alice <- getSMPAgentClient cfg + bob <- getSMPAgentClient cfg {dbFile = testDB2} + Right () <- runExceptT $ do + (bobId, qInfo) <- createConnection alice + disconnectAgentClient alice + aliceId <- joinConnection bob qInfo "bob's connInfo" + disconnectAgentClient bob + alice' <- liftIO $ getSMPAgentClient cfg + subscribeConnection alice' bobId + ("", _, CONF confId "bob's connInfo") <- get alice' + allowConnection alice' bobId confId "alice's connInfo" + bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} + subscribeConnection bob' aliceId + get alice' ##> ("", bobId, CON) + get bob' ##> ("", aliceId, INFO "alice's connInfo") + get bob' ##> ("", aliceId, CON) + exchangeGreetings alice' bobId bob' aliceId + pure () + +exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () +exchangeGreetings alice bobId bob aliceId = do + InternalId 1 <- sendMessage alice bobId "hello" + get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + InternalId 2 <- sendMessage bob aliceId "hello too" + get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False diff --git a/tests/Test.hs b/tests/Test.hs index 64ee00d5c..b27b86d59 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -1,6 +1,6 @@ {-# LANGUAGE TypeApplications #-} -import AgentTests +import AgentTests (agentTests) import ProtocolErrorTests import ServerTests import Simplex.Messaging.Transport (TCP, Transport (..)) From 10fcb9771a1e15dec8cab652471ba3657f5176f7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 20 Jul 2021 21:21:30 +0100 Subject: [PATCH 11/29] use explicit immediate transactions in all functions that change db (#170) --- src/Simplex/Messaging/Agent/Store/SQLite.hs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index ca6a088fa..6f18a9f8b 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -206,7 +206,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto deleteConn :: SQLiteStore -> ConnId -> m () deleteConn st connId = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db "DELETE FROM connections WHERE conn_alias = :conn_alias;" @@ -239,7 +239,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setRcvQueueStatus :: SQLiteStore -> RcvQueue -> QueueStatus -> m () setRcvQueueStatus st RcvQueue {rcvId, server = SMPServer {host, port}} status = -- ? throw error if queue does not exist? - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -252,7 +252,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setRcvQueueActive :: SQLiteStore -> RcvQueue -> VerificationKey -> m () setRcvQueueActive st RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey = -- ? throw error if queue does not exist? - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -270,7 +270,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setSndQueueStatus :: SQLiteStore -> SndQueue -> QueueStatus -> m () setSndQueueStatus st SndQueue {sndId, server = SMPServer {host, port}} status = -- ? throw error if queue does not exist? - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -282,7 +282,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto updateSignKey :: SQLiteStore -> SndQueue -> SignatureKey -> m () updateSignKey st SndQueue {sndId, server = SMPServer {host, port}} signatureKey = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -351,7 +351,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto removeConfirmations :: SQLiteStore -> ConnId -> m () removeConfirmations st connId = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -397,7 +397,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto createIntro :: SQLiteStore -> TVar ChaChaDRG -> NewIntroduction -> m IntroId createIntro st gVar NewIntroduction {toConn, reConn, reInfo} = - liftIOEither . withConnection st $ \db -> + liftIOEither . withTransaction st $ \db -> createWithRandomId gVar $ \introId -> DB.execute db @@ -426,7 +426,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto addIntroInvitation :: SQLiteStore -> IntroId -> ConnInfo -> SMPQueueInfo -> m () addIntroInvitation st introId toInfo qInfo = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -444,7 +444,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setIntroToStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () setIntroToStatus st introId toStatus = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.execute db [sql| @@ -456,7 +456,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setIntroReStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () setIntroReStatus st introId reStatus = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.execute db [sql| @@ -468,7 +468,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId createInvitation st gVar NewInvitation {viaConn, externalIntroId, connInfo, qInfo} = - liftIOEither . withConnection st $ \db -> + liftIOEither . withTransaction st $ \db -> createWithRandomId gVar $ \invId -> DB.execute db @@ -497,7 +497,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto addInvitationConn :: SQLiteStore -> InvitationId -> ConnId -> m () addInvitationConn st invId connId = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.executeNamed db [sql| @@ -531,7 +531,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto setInvitationStatus :: SQLiteStore -> InvitationId -> InvitationStatus -> m () setInvitationStatus st invId status = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> DB.execute db [sql| From 30c36b488a75d9282a0221fcc5ab019c9c4a49c5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 25 Jul 2021 20:27:09 +0100 Subject: [PATCH 12/29] commit simplexmq.cabal to remove stack DEPRECATED warning (#172) --- .gitignore | 1 - simplexmq.cabal | 249 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 simplexmq.cabal diff --git a/.gitignore b/.gitignore index 83446fcf4..965b1e528 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.lock -*.cabal *.db *.db.bak *.session.sql diff --git a/simplexmq.cabal b/simplexmq.cabal new file mode 100644 index 000000000..0c39fd79b --- /dev/null +++ b/simplexmq.cabal @@ -0,0 +1,249 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.34.4. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: 576ad28116836a490d6974cd6322169dd38b5df984c16fe296387427b5bef3d5 + +name: simplexmq +version: 0.3.2 +synopsis: SimpleXMQ message broker +description: This package includes <./docs/Simplex-Messaging-Server.html server>, + <./docs/Simplex-Messaging-Client.html client> and + <./docs/Simplex-Messaging-Agent.html agent> for SMP protocols: + . + * + * + . + See built with SimpleXMQ broker. +category: Chat, Network, Web, System, Cryptography +homepage: https://github.com/simplex-chat/simplexmq#readme +author: simplex.chat +maintainer: chat@simplex.chat +copyright: 2020 simplex.chat +license: AGPL-3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +library + exposed-modules: + Simplex.Messaging.Agent + Simplex.Messaging.Agent.Client + Simplex.Messaging.Agent.Env.SQLite + Simplex.Messaging.Agent.Protocol + Simplex.Messaging.Agent.Store + Simplex.Messaging.Agent.Store.SQLite + Simplex.Messaging.Agent.Store.SQLite.Migrations + Simplex.Messaging.Client + Simplex.Messaging.Crypto + Simplex.Messaging.Parsers + Simplex.Messaging.Protocol + Simplex.Messaging.Server + Simplex.Messaging.Server.Env.STM + Simplex.Messaging.Server.MsgStore + Simplex.Messaging.Server.MsgStore.STM + Simplex.Messaging.Server.QueueStore + Simplex.Messaging.Server.QueueStore.STM + Simplex.Messaging.Server.StoreLog + Simplex.Messaging.Transport + Simplex.Messaging.Transport.WebSockets + Simplex.Messaging.Util + other-modules: + Paths_simplexmq + hs-source-dirs: + src + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns + build-depends: + QuickCheck ==2.14.* + , ansi-terminal >=0.10 && <0.12 + , asn1-encoding ==0.9.* + , asn1-types ==0.3.* + , async ==2.2.* + , attoparsec ==0.13.* + , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 + , bytestring ==0.10.* + , composition ==1.0.* + , constraints >=0.12 && <0.14 + , containers ==0.6.* + , cryptonite >=0.27 && <0.30 + , direct-sqlite ==2.3.* + , directory ==1.3.* + , file-embed ==0.0.14.* + , filepath ==1.4.* + , generic-random >=1.3 && <1.5 + , iso8601-time ==0.1.* + , memory ==0.15.* + , mtl ==2.2.* + , network ==3.1.* + , network-transport ==0.5.* + , random >=1.1 && <1.3 + , simple-logger ==0.1.* + , sqlite-simple ==0.4.* + , stm ==2.5.* + , template-haskell ==2.16.* + , text ==1.2.* + , time ==1.9.* + , transformers ==0.5.* + , unliftio ==0.2.* + , unliftio-core ==0.2.* + , websockets ==0.12.* + , x509 ==1.7.* + default-language: Haskell2010 + +executable smp-agent + main-is: Main.hs + other-modules: + Paths_simplexmq + hs-source-dirs: + apps/smp-agent + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + build-depends: + QuickCheck ==2.14.* + , ansi-terminal >=0.10 && <0.12 + , asn1-encoding ==0.9.* + , asn1-types ==0.3.* + , async ==2.2.* + , attoparsec ==0.13.* + , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 + , bytestring ==0.10.* + , composition ==1.0.* + , constraints >=0.12 && <0.14 + , containers ==0.6.* + , cryptonite >=0.27 && <0.30 + , direct-sqlite ==2.3.* + , directory ==1.3.* + , file-embed ==0.0.14.* + , filepath ==1.4.* + , generic-random >=1.3 && <1.5 + , iso8601-time ==0.1.* + , memory ==0.15.* + , mtl ==2.2.* + , network ==3.1.* + , network-transport ==0.5.* + , random >=1.1 && <1.3 + , simple-logger ==0.1.* + , simplexmq + , sqlite-simple ==0.4.* + , stm ==2.5.* + , template-haskell ==2.16.* + , text ==1.2.* + , time ==1.9.* + , transformers ==0.5.* + , unliftio ==0.2.* + , unliftio-core ==0.2.* + , websockets ==0.12.* + , x509 ==1.7.* + default-language: Haskell2010 + +executable smp-server + main-is: Main.hs + other-modules: + Paths_simplexmq + hs-source-dirs: + apps/smp-server + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + build-depends: + QuickCheck ==2.14.* + , ansi-terminal >=0.10 && <0.12 + , asn1-encoding ==0.9.* + , asn1-types ==0.3.* + , async ==2.2.* + , attoparsec ==0.13.* + , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 + , bytestring ==0.10.* + , composition ==1.0.* + , constraints >=0.12 && <0.14 + , containers ==0.6.* + , cryptonite >=0.27 && <0.30 + , cryptostore ==0.2.* + , direct-sqlite ==2.3.* + , directory ==1.3.* + , file-embed ==0.0.14.* + , filepath ==1.4.* + , generic-random >=1.3 && <1.5 + , ini ==0.4.* + , iso8601-time ==0.1.* + , memory ==0.15.* + , mtl ==2.2.* + , network ==3.1.* + , network-transport ==0.5.* + , optparse-applicative >=0.15 && <0.17 + , random >=1.1 && <1.3 + , simple-logger ==0.1.* + , simplexmq + , sqlite-simple ==0.4.* + , stm ==2.5.* + , template-haskell ==2.16.* + , text ==1.2.* + , time ==1.9.* + , transformers ==0.5.* + , unliftio ==0.2.* + , unliftio-core ==0.2.* + , websockets ==0.12.* + , x509 ==1.7.* + default-language: Haskell2010 + +test-suite smp-server-test + type: exitcode-stdio-1.0 + main-is: Test.hs + other-modules: + AgentTests + AgentTests.FunctionalAPITests + AgentTests.SQLiteTests + ProtocolErrorTests + ServerTests + SMPAgentClient + SMPClient + Paths_simplexmq + hs-source-dirs: + tests + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns + build-depends: + HUnit ==1.6.* + , QuickCheck ==2.14.* + , ansi-terminal >=0.10 && <0.12 + , asn1-encoding ==0.9.* + , asn1-types ==0.3.* + , async ==2.2.* + , attoparsec ==0.13.* + , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 + , bytestring ==0.10.* + , composition ==1.0.* + , constraints >=0.12 && <0.14 + , containers ==0.6.* + , cryptonite >=0.27 && <0.30 + , direct-sqlite ==2.3.* + , directory ==1.3.* + , file-embed ==0.0.14.* + , filepath ==1.4.* + , generic-random >=1.3 && <1.5 + , hspec ==2.7.* + , hspec-core ==2.7.* + , iso8601-time ==0.1.* + , memory ==0.15.* + , mtl ==2.2.* + , network ==3.1.* + , network-transport ==0.5.* + , random >=1.1 && <1.3 + , simple-logger ==0.1.* + , simplexmq + , sqlite-simple ==0.4.* + , stm ==2.5.* + , template-haskell ==2.16.* + , text ==1.2.* + , time ==1.9.* + , timeit ==2.0.* + , transformers ==0.5.* + , unliftio ==0.2.* + , unliftio-core ==0.2.* + , websockets ==0.12.* + , x509 ==1.7.* + default-language: Haskell2010 From 8a4bced56972363c073c05bf81ecc0a3b1c2cd8d Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sun, 1 Aug 2021 22:51:40 +1000 Subject: [PATCH 13/29] decrease initial delay for HELLO retries on online activation (#174) * decrease initial delay for HELLO retries on online activation * move retry interval to config Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/smp-agent/Main.hs | 13 +-------- src/Simplex/Messaging/Agent.hs | 33 +++++++---------------- src/Simplex/Messaging/Agent/Client.hs | 6 ----- src/Simplex/Messaging/Agent/Env/SQLite.hs | 32 +++++++++++++++++++++- tests/SMPAgentClient.hs | 8 +++--- 5 files changed, 45 insertions(+), 47 deletions(-) diff --git a/apps/smp-agent/Main.hs b/apps/smp-agent/Main.hs index a632c63f9..bb0685549 100644 --- a/apps/smp-agent/Main.hs +++ b/apps/smp-agent/Main.hs @@ -8,21 +8,10 @@ import Control.Logger.Simple import qualified Data.List.NonEmpty as L import Simplex.Messaging.Agent (runSMPAgent) import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Client (smpDefaultConfig) import Simplex.Messaging.Transport (TCP, Transport (..)) cfg :: AgentConfig -cfg = - AgentConfig - { tcpPort = "5224", - smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="], - rsaKeySize = 2048 `div` 8, - connIdBytes = 12, - tbqSize = 16, - dbFile = "smp-agent.db", - dbPoolSize = 4, - smpCfg = smpDefaultConfig - } +cfg = defaultAgentConfig {smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="]} logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 53599d709..69a3864c6 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -270,33 +270,15 @@ newConn c connId viaInv connLevel = do addSubscription c rq connId' pure (connId', qInfo) -minute :: Int -minute = 60_000_000 - -onlineInterval :: RetryInterval -onlineInterval = - RetryInterval - { initialInterval = 1_000_000, - increaseAfter = minute, - maxInterval = 10 * minute - } - -resumeInterval :: RetryInterval -resumeInterval = - RetryInterval - { initialInterval = 5_000_000, - increaseAfter = 0, - maxInterval = 10 * minute - } - joinConn :: AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ConnInfo -> Maybe InvitationId -> Int -> m ConnId joinConn c connId qInfo cInfo viaInv connLevel = do (sq, senderKey, verifyKey) <- newSndQueue qInfo g <- asks idsDrg + cfg <- asks config let cData = ConnData {connId, viaInv, connLevel} connId' <- withStore $ \st -> createSndConn st g cData sq confirmQueue c sq senderKey cInfo - activateQueueJoining c connId' sq verifyKey onlineInterval + activateQueueJoining c connId' sq verifyKey $ retryInterval cfg pure connId' activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () @@ -370,7 +352,7 @@ subscribeConnection' c connId = _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (SndConnection _ sq) -> case status (sq :: SndQueue) of Confirmed -> withVerifyKey sq $ \verifyKey -> - activateQueueJoining c connId sq verifyKey resumeInterval + activateQueueJoining c connId sq verifyKey =<< resumeInterval Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId @@ -381,8 +363,12 @@ subscribeConnection' c connId = in maybe err action . C.publicKey $ signKey sq activateSecuredQueue :: RcvQueue -> SndQueue -> C.PublicKey -> m () activateSecuredQueue rq sq verifyKey = do - activateQueueInitiating c connId sq verifyKey resumeInterval + activateQueueInitiating c connId sq verifyKey =<< resumeInterval subscribeQueue c rq connId + resumeInterval :: m RetryInterval + resumeInterval = do + r <- asks $ retryInterval . config + pure r {initialInterval = 5_000_000} -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId @@ -540,7 +526,8 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do withStore $ \st -> upgradeRcvConnToDuplex st connId sq confirmQueue c sq senderKey ownConnInfo withStore (`removeConfirmations` connId) - activateQueueInitiating c connId sq verifyKey onlineInterval + cfg <- asks config + activateQueueInitiating c connId sq verifyKey $ retryInterval cfg _ -> prohibited introMsg :: IntroId -> ConnInfo -> m () diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index b4ff3069a..9ffbd44e0 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -254,12 +254,6 @@ sendConfirmation c sq@SndQueue {server, sndId} senderKey cInfo = mkConfirmation :: SMPClient -> m MsgBody mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey cInfo -data RetryInterval = RetryInterval - { initialInterval :: Int, - increaseAfter :: Int, - maxInterval :: Int - } - sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m () sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey RetryInterval {initialInterval, increaseAfter, maxInterval} = withLogSMP_ c server sndId "SEND (retrying)" $ \smp -> do diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 5fbe69739..85877aa0d 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} module Simplex.Messaging.Agent.Env.SQLite where @@ -25,9 +26,38 @@ data AgentConfig = AgentConfig tbqSize :: Natural, dbFile :: FilePath, dbPoolSize :: Int, - smpCfg :: SMPClientConfig + smpCfg :: SMPClientConfig, + retryInterval :: RetryInterval } +minute :: Int +minute = 60_000_000 + +data RetryInterval = RetryInterval + { initialInterval :: Int, + increaseAfter :: Int, + maxInterval :: Int + } + +defaultAgentConfig :: AgentConfig +defaultAgentConfig = + AgentConfig + { tcpPort = "5224", + smpServers = undefined, + rsaKeySize = 2048 `div` 8, + connIdBytes = 12, + tbqSize = 16, + dbFile = "smp-agent.db", + dbPoolSize = 4, + smpCfg = smpDefaultConfig, + retryInterval = + RetryInterval + { initialInterval = 1_000_000, + increaseAfter = minute, + maxInterval = 10 * minute + } + } + data Env = Env { config :: AgentConfig, store :: SQLiteStore, diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index f30702cf6..af3093ca0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -141,20 +141,18 @@ smpAgentTest3_1_1 test' = smpAgentTestN_1 3 _test cfg :: AgentConfig cfg = - AgentConfig + defaultAgentConfig { tcpPort = agentTestPort, smpServers = L.fromList ["localhost:5000#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="], - rsaKeySize = 2048 `div` 8, - connIdBytes = 12, tbqSize = 1, dbFile = testDB, - dbPoolSize = 4, smpCfg = smpDefaultConfig { qSize = 1, defaultTransport = (testPort, transport @TCP), tcpTimeout = 500_000 - } + }, + retryInterval = (retryInterval defaultAgentConfig) {initialInterval = 50_000} } withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a From 3829479a21c42714ad988f8a3b858dfa92e508f6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 5 Aug 2021 08:27:44 +0100 Subject: [PATCH 14/29] Remove introductions, rename CONF/LET to REQ/ACPT (#175) * remove intros and functions in Reader monad * rename CONF/LET to REQ/ACPT, allowConnection to acceptConnection --- migrations/20210602_introductions.sql | 27 --- src/Simplex/Messaging/Agent.hs | 162 ++---------------- src/Simplex/Messaging/Agent/Protocol.hs | 61 +------ src/Simplex/Messaging/Agent/Store.hs | 90 +--------- src/Simplex/Messaging/Agent/Store/SQLite.hs | 181 ++------------------ tests/AgentTests.hs | 70 +------- tests/AgentTests/FunctionalAPITests.hs | 16 +- tests/AgentTests/SQLiteTests.hs | 2 +- 8 files changed, 56 insertions(+), 553 deletions(-) delete mode 100644 migrations/20210602_introductions.sql diff --git a/migrations/20210602_introductions.sql b/migrations/20210602_introductions.sql deleted file mode 100644 index 36bb7539c..000000000 --- a/migrations/20210602_introductions.sql +++ /dev/null @@ -1,27 +0,0 @@ -CREATE TABLE conn_intros ( - intro_id BLOB NOT NULL PRIMARY KEY, - to_conn BLOB NOT NULL REFERENCES connections (conn_alias) ON DELETE CASCADE, - to_info BLOB, -- info about "to" connection sent to "re" connection - to_status TEXT NOT NULL DEFAULT '', -- '', INV, CON - re_conn BLOB NOT NULL REFERENCES connections (conn_alias) ON DELETE CASCADE, - re_info BLOB NOT NULL, -- info about "re" connection sent to "to" connection - re_status TEXT NOT NULL DEFAULT '', -- '', INV, CON - queue_info BLOB -) WITHOUT ROWID; - -CREATE TABLE conn_invitations ( - inv_id BLOB NOT NULL PRIMARY KEY, - via_conn BLOB REFERENCES connections (conn_alias) ON DELETE SET NULL, - external_intro_id BLOB NOT NULL, - conn_info BLOB, -- info about another connection - queue_info BLOB, -- NULL if it's an initial introduction - conn_id BLOB REFERENCES connections (conn_alias) -- created connection - ON DELETE CASCADE - DEFERRABLE INITIALLY DEFERRED, - status TEXT NOT NULL DEFAULT '' -- '', 'ACPT', 'CON' -) WITHOUT ROWID; - -ALTER TABLE connections - ADD via_inv BLOB REFERENCES conn_invitations (inv_id) ON DELETE RESTRICT; -ALTER TABLE connections - ADD conn_level INTEGER NOT NULL DEFAULT 0; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 69a3864c6..d66d2184e 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -41,22 +41,11 @@ module Simplex.Messaging.Agent disconnectAgentClient, -- used in tests createConnection, joinConnection, - allowConnection, - sendIntroduction, - acceptInvitation, + acceptConnection, subscribeConnection, sendMessage, suspendConnection, deleteConnection, - createConnection', - joinConnection', - allowConnection', - sendIntroduction', - acceptInvitation', - subscribeConnection', - sendMessage', - suspendConnection', - deleteConnection', ) where @@ -131,37 +120,17 @@ disconnectAgentClient c = closeAgentClient c >> logConnection c False -- | type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) --- | Create SMP agent connection (NEW command) in Reader monad -createConnection' :: AgentMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection' c = newConn c "" Nothing 0 - -- | Create SMP agent connection (NEW command) createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection c = (`runReaderT` agentEnv c) $ createConnection' c - --- | Join SMP agent connection (JOIN command) in Reader monad -joinConnection' :: AgentMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection' c qInfo cInfo = joinConn c "" qInfo cInfo Nothing 0 +createConnection c = (`runReaderT` agentEnv c) $ newConn c "" -- | Join SMP agent connection (JOIN command) joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection c = (`runReaderT` agentEnv c) .: joinConnection' c +joinConnection c = (`runReaderT` agentEnv c) .: joinConn c "" -- | Approve confirmation (LET command) -allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -allowConnection c = (`runReaderT` agentEnv c) .:. allowConnection' c - --- | Accept invitation (ACPT command) in Reader monad -acceptInvitation' :: AgentMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId -acceptInvitation' c = acceptInv c "" - --- | Accept invitation (ACPT command) -acceptInvitation :: AgentErrorMonad m => AgentClient -> InvitationId -> ConnInfo -> m ConnId -acceptInvitation c = (`runReaderT` agentEnv c) .: acceptInvitation' c - --- | Send introduction of the second connection the first (INTRO command) -sendIntroduction :: AgentErrorMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () -sendIntroduction c = (`runReaderT` agentEnv c) .:. sendIntroduction' c +acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +acceptConnection c = (`runReaderT` agentEnv c) .:. acceptConnection' c -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () @@ -250,32 +219,30 @@ withStore action = do -- | execute any SMP agent command processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent) processCommand c (connId, cmd) = case cmd of - NEW -> second INV <$> newConn c connId Nothing 0 - JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo Nothing 0 - LET confId ownConnInfo -> allowConnection' c connId confId ownConnInfo $> (connId, OK) - INTRO reConnId reInfo -> sendIntroduction' c connId reConnId reInfo $> (connId, OK) - ACPT invId connInfo -> (,OK) <$> acceptInv c connId invId connInfo + NEW -> second INV <$> newConn c connId + JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo + ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . SENT . unId <$> sendMessage' c connId msgBody OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) -newConn :: AgentMonad m => AgentClient -> ConnId -> Maybe InvitationId -> Int -> m (ConnId, SMPQueueInfo) -newConn c connId viaInv connLevel = do +newConn :: AgentMonad m => AgentClient -> ConnId -> m (ConnId, SMPQueueInfo) +newConn c connId = do srv <- getSMPServer (rq, qInfo) <- newRcvQueue c srv g <- asks idsDrg - let cData = ConnData {connId, viaInv, connLevel} + let cData = ConnData {connId} connId' <- withStore $ \st -> createRcvConn st g cData rq addSubscription c rq connId' pure (connId', qInfo) -joinConn :: AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ConnInfo -> Maybe InvitationId -> Int -> m ConnId -joinConn c connId qInfo cInfo viaInv connLevel = do +joinConn :: AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConn c connId qInfo cInfo = do (sq, senderKey, verifyKey) <- newSndQueue qInfo g <- asks idsDrg cfg <- asks config - let cData = ConnData {connId, viaInv, connLevel} + let cData = ConnData {connId} connId' <- withStore $ \st -> createSndConn st g cData sq confirmQueue c sq senderKey cInfo activateQueueJoining c connId' sq verifyKey $ retryInterval cfg @@ -294,8 +261,8 @@ activateQueueJoining c connId sq verifyKey retryInterval = sendControlMessage c sq $ REPLY qInfo' -- | Approve confirmation (LET command) in Reader monad -allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -allowConnection' c connId confId ownConnInfo = +acceptConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +acceptConnection' c connId confId ownConnInfo = withStore (`getConn` connId) >>= \case SomeConn SCRcv (RcvConnection _ rq) -> do AcceptedConfirmation {senderKey} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo @@ -308,35 +275,6 @@ processConfirmation c rq sndKey = do secureQueue c rq sndKey withStore $ \st -> setRcvQueueStatus st rq Secured --- | Send introduction of the second connection the first (INTRO command) in Reader monad -sendIntroduction' :: AgentMonad m => AgentClient -> ConnId -> ConnId -> ConnInfo -> m () -sendIntroduction' c toConn reConn reInfo = - withStore (\st -> (,) <$> getConn st toConn <*> getConn st reConn) >>= \case - (SomeConn _ (DuplexConnection _ _ sq), SomeConn _ DuplexConnection {}) -> do - g <- asks idsDrg - introId <- withStore $ \st -> createIntro st g NewIntroduction {toConn, reConn, reInfo} - sendControlMessage c sq $ A_INTRO introId reInfo - _ -> throwError $ CONN SIMPLEX - -acceptInv :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId -acceptInv c connId invId connInfo = - withStore (`getInvitation` invId) >>= \case - Invitation {viaConn, qInfo, externalIntroId, status = InvNew} -> - withStore (`getConn` viaConn) >>= \case - SomeConn _ (DuplexConnection ConnData {connLevel} _ sq) -> case qInfo of - Nothing -> do - (connId', qInfo') <- newConn c connId (Just invId) (connLevel + 1) - withStore $ \st -> addInvitationConn st invId connId' - sendControlMessage c sq $ A_INV externalIntroId qInfo' connInfo - pure connId' - Just qInfo' -> do - -- TODO remove invitations from protocol - connId' <- joinConn c connId qInfo' connInfo (Just invId) (connLevel + 1) - withStore $ \st -> addInvitationConn st invId connId' - pure connId' - _ -> throwError $ CONN SIMPLEX - _ -> throwError $ CMD PROHIBITED - -- | Subscribe to receive connection messages (SUB command) in Reader monad subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = @@ -468,10 +406,6 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do HELLO verifyKey _ -> helloMsg verifyKey msgBody REPLY qInfo -> replyMsg qInfo A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash - A_INTRO introId cInfo -> introMsg introId cInfo - A_INV introId qInfo cInfo -> invMsg introId qInfo cInfo - A_REQ introId qInfo cInfo -> reqMsg introId qInfo cInfo - A_CON introId -> conMsg introId sendAck c rq return () SMP.END -> do @@ -497,7 +431,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do g <- asks idsDrg let newConfirmation = NewConfirmation {connId, senderKey, senderConnInfo = cInfo} confId <- withStore $ \st -> createConfirmation st g newConfirmation - notify $ CONF confId cInfo + notify $ REQ confId cInfo SCDuplex -> do notify $ INFO cInfo processConfirmation c rq senderKey @@ -530,60 +464,6 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do activateQueueInitiating c connId sq verifyKey $ retryInterval cfg _ -> prohibited - introMsg :: IntroId -> ConnInfo -> m () - introMsg introId reInfo = do - logServer "<--" c srv rId "MSG " - case cType of - SCDuplex -> createInv introId Nothing reInfo - _ -> prohibited - - invMsg :: IntroId -> SMPQueueInfo -> ConnInfo -> m () - invMsg introId qInfo toInfo = do - logServer "<--" c srv rId "MSG " - case cType of - SCDuplex -> - withStore (`getIntro` introId) >>= \case - Introduction {toConn, toStatus = IntroNew, reConn, reStatus = IntroNew} - | toConn /= connId -> prohibited - | otherwise -> - withStore (\st -> addIntroInvitation st introId toInfo qInfo >> getConn st reConn) >>= \case - SomeConn _ (DuplexConnection _ _ sq) -> do - sendControlMessage c sq $ A_REQ introId qInfo toInfo - withStore $ \st -> setIntroReStatus st introId IntroInv - _ -> prohibited - _ -> prohibited - _ -> prohibited - - reqMsg :: IntroId -> SMPQueueInfo -> ConnInfo -> m () - reqMsg introId qInfo connInfo = do - logServer "<--" c srv rId "MSG " - case cType of - SCDuplex -> createInv introId (Just qInfo) connInfo - _ -> prohibited - - createInv :: IntroId -> Maybe SMPQueueInfo -> ConnInfo -> m () - createInv externalIntroId qInfo connInfo = do - g <- asks idsDrg - let newInv = NewInvitation {viaConn = connId, externalIntroId, connInfo, qInfo} - invId <- withStore $ \st -> createInvitation st g newInv - notify $ REQ invId connInfo - - conMsg :: IntroId -> m () - conMsg introId = do - logServer "<--" c srv rId "MSG " - withStore (`getIntro` introId) >>= \case - Introduction {toConn, toStatus, reConn, reStatus} - | toConn == connId && toStatus == IntroInv -> do - withStore $ \st -> setIntroToStatus st introId IntroCon - when (reStatus == IntroCon) $ sendConMsg toConn reConn - | reConn == connId && reStatus == IntroInv -> do - withStore $ \st -> setIntroReStatus st introId IntroCon - when (toStatus == IntroCon) $ sendConMsg toConn reConn - | otherwise -> prohibited - where - sendConMsg :: ConnId -> ConnId -> m () - sendConMsg toConn reConn = atomically $ writeTBQueue subQ ("", toConn, ICON reConn) - agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () agentClientMsg externalPrevSndHash sender broker msgBody internalHash = do logServer "<--" c srv rId "MSG " @@ -636,13 +516,7 @@ activateQueue c connId sq verifyKey retryInterval afterActivation = in withStore $ \st -> updateSignKey st sq safeSignKey notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () -notifyConnected c connId = do - withStore (`getConnInvitation` connId) >>= \case - Just (Invitation {invId, externalIntroId}, DuplexConnection _ _ sq) -> do - withStore $ \st -> setInvitationStatus st invId InvCon - sendControlMessage c sq $ A_CON externalIntroId - _ -> pure () - atomically $ writeTBQueue (subQ c) ("", connId, CON) +notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 590ca63c5..b8f29e856 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -156,14 +156,10 @@ data ACommand (p :: AParty) where NEW :: ACommand Client -- response INV INV :: SMPQueueInfo -> ACommand Agent JOIN :: SMPQueueInfo -> ConnInfo -> ACommand Client -- response OK - CONF :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender - LET :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client - INTRO :: ConnId -> ConnInfo -> ACommand Client - REQ :: InvitationId -> ConnInfo -> ACommand Agent - ACPT :: InvitationId -> ConnInfo -> ACommand Client + REQ :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender + ACPT :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client INFO :: ConnInfo -> ACommand Agent CON :: ACommand Agent -- notification that connection is established - ICON :: ConnId -> ACommand Agent SUB :: ACommand Client END :: ACommand Agent -- QST :: QueueDirection -> ACommand Client @@ -225,14 +221,6 @@ data AMessage where REPLY :: SMPQueueInfo -> AMessage -- | agent envelope for the client message A_MSG :: MsgBody -> AMessage - -- | agent message for introduction - A_INTRO :: IntroId -> ConnInfo -> AMessage - -- | agent envelope for the sent invitation - A_INV :: IntroId -> SMPQueueInfo -> ConnInfo -> AMessage - -- | agent envelope for the forwarded invitation - A_REQ :: IntroId -> SMPQueueInfo -> ConnInfo -> AMessage - -- | agent message for intro/group request - A_CON :: IntroId -> AMessage deriving (Show) -- | Parse SMP message. @@ -273,19 +261,10 @@ agentMessageP = "HELLO " *> hello <|> "REPLY " *> reply <|> "MSG " *> a_msg - <|> "INTRO " *> a_intro - <|> "INV " *> a_inv - <|> "REQ " *> a_req - <|> "CON " *> a_con where hello = HELLO <$> C.pubKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP a_msg = A_MSG <$> binaryBodyP <* A.endOfLine - a_intro = A_INTRO <$> A.takeTill (== ' ') <* A.space <*> binaryBodyP <* A.endOfLine - a_inv = invP A_INV - a_req = invP A_REQ - a_con = A_CON <$> A.takeTill wordEnd - invP f = f <$> A.takeTill (== ' ') <* A.space <*> smpQueueInfoP <* A.space <*> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) -- | SMP queue information parser. @@ -306,13 +285,6 @@ serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo A_MSG body -> "MSG " <> serializeBinary body <> "\n" - A_INTRO introId cInfo -> "INTRO " <> introId <> " " <> serializeBinary cInfo <> "\n" - A_INV introId qInfo cInfo -> "INV " <> serializeInv introId qInfo cInfo - A_REQ introId qInfo cInfo -> "REQ " <> serializeInv introId qInfo cInfo - A_CON introId -> "CON " <> introId - where - serializeInv introId qInfo cInfo = - B.intercalate " " [introId, serializeSmpQueueInfo qInfo, serializeBinary cInfo] <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString @@ -478,9 +450,6 @@ commandP = "NEW" $> ACmd SClient NEW <|> "INV " *> invResp <|> "JOIN " *> joinCmd - <|> "CONF " *> confCmd - <|> "LET " *> letCmd - <|> "INTRO " *> introCmd <|> "REQ " *> reqCmd <|> "ACPT " *> acptCmd <|> "INFO " *> infoCmd @@ -492,21 +461,16 @@ commandP = <|> "OFF" $> ACmd SClient OFF <|> "DEL" $> ACmd SClient DEL <|> "ERR " *> agentError - <|> "ICON " *> iconMsg <|> "CON" $> ACmd SAgent CON <|> "OK" $> ACmd SAgent OK where invResp = ACmd SAgent . INV <$> smpQueueInfoP joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <* A.space <*> A.takeByteString) - confCmd = ACmd SAgent <$> (CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) - letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) - introCmd = ACmd SClient <$> introP INTRO - reqCmd = ACmd SAgent <$> introP REQ - acptCmd = ACmd SClient <$> introP ACPT + reqCmd = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString sentResp = ACmd SAgent . SENT <$> A.decimal - iconMsg = ACmd SAgent . ICON <$> A.takeTill wordEnd message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString) msgMetaP = do integrity <- msgIntegrityP @@ -514,7 +478,6 @@ commandP = broker <- " B=" *> partyMeta base64P sender <- " S=" *> partyMeta A.decimal pure MsgMeta {integrity, recipient, broker, sender} - introP f = f <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P agentError = ACmd SAgent . ERR <$> agentErrorTypeP @@ -537,11 +500,8 @@ serializeCommand = \case NEW -> "NEW" INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo JOIN qInfo cInfo -> "JOIN " <> serializeSmpQueueInfo qInfo <> " " <> serializeBinary cInfo - CONF confId cInfo -> "CONF " <> confId <> " " <> serializeBinary cInfo - LET confId cInfo -> "LET " <> confId <> " " <> serializeBinary cInfo - INTRO connId cInfo -> "INTRO " <> connId <> " " <> serializeBinary cInfo - REQ invId cInfo -> "REQ " <> invId <> " " <> serializeBinary cInfo - ACPT invId cInfo -> "ACPT " <> invId <> " " <> serializeBinary cInfo + REQ confId cInfo -> "REQ " <> confId <> " " <> serializeBinary cInfo + ACPT confId cInfo -> "ACPT " <> confId <> " " <> serializeBinary cInfo INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" @@ -552,7 +512,6 @@ serializeCommand = \case OFF -> "OFF" DEL -> "DEL" CON -> "CON" - ICON connId -> "ICON " <> connId ERR e -> "ERR " <> serializeAgentError e OK -> "OK" where @@ -640,7 +599,6 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody -- NEW, JOIN and ACPT have optional connId NEW -> Right cmd JOIN {} -> Right cmd - ACPT {} -> Right cmd -- ERROR response does not always have connId ERR _ -> Right cmd -- other responses must have connId @@ -652,12 +610,9 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody cmdWithMsgBody = \case SEND body -> SEND <$$> getBody body MSG msgMeta body -> MSG msgMeta <$$> getBody body - INTRO introId cInfo -> INTRO introId <$$> getBody cInfo - REQ introId cInfo -> REQ introId <$$> getBody cInfo - ACPT introId cInfo -> ACPT introId <$$> getBody cInfo JOIN qInfo cInfo -> JOIN qInfo <$$> getBody cInfo - CONF confId cInfo -> CONF confId <$$> getBody cInfo - LET confId cInfo -> LET confId <$$> getBody cInfo + REQ confId cInfo -> REQ confId <$$> getBody cInfo + ACPT confId cInfo -> ACPT confId <$$> getBody cInfo INFO cInfo -> INFO <$$> getBody cInfo cmd -> pure $ Right cmd diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 6b49ad280..72f157397 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -3,9 +3,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} @@ -17,7 +15,6 @@ import Crypto.Random (ChaChaDRG) import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Kind (Type) -import Data.Text (Text) import Data.Time (UTCTime) import Data.Type.Equality import Simplex.Messaging.Agent.Protocol @@ -61,18 +58,6 @@ class Monad m => MonadAgentStore s m where createSndMsg :: s -> ConnId -> SndMsgData -> m () getMsg :: s -> ConnId -> InternalId -> m Msg - -- Introductions - createIntro :: s -> TVar ChaChaDRG -> NewIntroduction -> m IntroId - getIntro :: s -> IntroId -> m Introduction - addIntroInvitation :: s -> IntroId -> ConnInfo -> SMPQueueInfo -> m () - setIntroToStatus :: s -> IntroId -> IntroStatus -> m () - setIntroReStatus :: s -> IntroId -> IntroStatus -> m () - createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId - getInvitation :: s -> InvitationId -> m Invitation - addInvitationConn :: s -> InvitationId -> ConnId -> m () - getConnInvitation :: s -> ConnId -> m (Maybe (Invitation, Connection CDuplex)) - setInvitationStatus :: s -> InvitationId -> InvitationStatus -> m () - -- * Queue types -- | A receive queue. SMP queue through which the agent receives messages from a sender. @@ -153,7 +138,7 @@ instance Eq SomeConn where deriving instance Show SomeConn -data ConnData = ConnData {connId :: ConnId, viaInv :: Maybe InvitationId, connLevel :: Int} +newtype ConnData = ConnData {connId :: ConnId} deriving (Eq, Show) -- * Confirmation types @@ -308,75 +293,6 @@ newtype InternalId = InternalId {unId :: Int64} deriving (Eq, Show) type InternalTs = UTCTime --- * Introduction types - -data NewIntroduction = NewIntroduction - { toConn :: ConnId, - reConn :: ConnId, - reInfo :: ByteString - } - -data Introduction = Introduction - { introId :: IntroId, - toConn :: ConnId, - toInfo :: Maybe ByteString, - toStatus :: IntroStatus, - reConn :: ConnId, - reInfo :: ByteString, - reStatus :: IntroStatus, - qInfo :: Maybe SMPQueueInfo - } - -data IntroStatus = IntroNew | IntroInv | IntroCon - deriving (Eq) - -serializeIntroStatus :: IntroStatus -> Text -serializeIntroStatus = \case - IntroNew -> "" - IntroInv -> "INV" - IntroCon -> "CON" - -introStatusT :: Text -> Maybe IntroStatus -introStatusT = \case - "" -> Just IntroNew - "INV" -> Just IntroInv - "CON" -> Just IntroCon - _ -> Nothing - -data NewInvitation = NewInvitation - { viaConn :: ConnId, - externalIntroId :: IntroId, - connInfo :: ConnInfo, - qInfo :: Maybe SMPQueueInfo - } - -data Invitation = Invitation - { invId :: InvitationId, - viaConn :: ConnId, - externalIntroId :: IntroId, - connInfo :: ConnInfo, - qInfo :: Maybe SMPQueueInfo, - connId :: Maybe ConnId, - status :: InvitationStatus - } - deriving (Show) - -data InvitationStatus = InvNew | InvAcpt | InvCon - deriving (Eq, Show) - -serializeInvStatus :: InvitationStatus -> Text -serializeInvStatus = \case - InvNew -> "" - InvAcpt -> "ACPT" - InvCon -> "CON" - -invStatusT :: Text -> Maybe InvitationStatus -invStatusT = \case - "" -> Just InvNew - "ACPT" -> Just InvAcpt - "CON" -> Just InvCon - _ -> Nothing - -- * Store errors -- | Agent store error. @@ -394,10 +310,6 @@ data StoreError SEBadConnType ConnType | -- | Confirmation not found. SEConfirmationNotFound - | -- | Introduction ID not found. - SEIntroNotFound - | -- | Invitation ID not found. - SEInvitationNotFound | -- | Currently not used. The intention was to pass current expected queue status in methods, -- as we always know what it should be at any stage of the protocol, -- and in case it does not match use this error. diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 6f18a9f8b..f6d2e2fd9 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -29,7 +29,7 @@ where import Control.Concurrent (threadDelay) import Control.Concurrent.STM import Control.Exception (bracket) -import Control.Monad (join, replicateM_, unless, when) +import Control.Monad (replicateM_, unless, when) import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) import Control.Monad.IO.Unlift (MonadUnliftIO) import Crypto.Random (ChaChaDRG, randomBytesGenerate) @@ -395,151 +395,6 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg getMsg _st _connAlias _id = throwError SENotImplemented - createIntro :: SQLiteStore -> TVar ChaChaDRG -> NewIntroduction -> m IntroId - createIntro st gVar NewIntroduction {toConn, reConn, reInfo} = - liftIOEither . withTransaction st $ \db -> - createWithRandomId gVar $ \introId -> - DB.execute - db - [sql| - INSERT INTO conn_intros - (intro_id, to_conn, re_conn, re_info) VALUES (?, ?, ?, ?); - |] - (introId, toConn, reConn, reInfo) - - getIntro :: SQLiteStore -> IntroId -> m Introduction - getIntro st introId = - liftIOEither . withConnection st $ \db -> - intro - <$> DB.query - db - [sql| - SELECT to_conn, to_info, to_status, re_conn, re_info, re_status, queue_info - FROM conn_intros - WHERE intro_id = ?; - |] - (Only introId) - where - intro [(toConn, toInfo, toStatus, reConn, reInfo, reStatus, qInfo)] = - Right $ Introduction {introId, toConn, toInfo, toStatus, reConn, reInfo, reStatus, qInfo} - intro _ = Left SEIntroNotFound - - addIntroInvitation :: SQLiteStore -> IntroId -> ConnInfo -> SMPQueueInfo -> m () - addIntroInvitation st introId toInfo qInfo = - liftIO . withTransaction st $ \db -> - DB.executeNamed - db - [sql| - UPDATE conn_intros - SET to_info = :to_info, - queue_info = :queue_info, - to_status = :to_status - WHERE intro_id = :intro_id; - |] - [ ":to_info" := toInfo, - ":queue_info" := Just qInfo, - ":to_status" := IntroInv, - ":intro_id" := introId - ] - - setIntroToStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () - setIntroToStatus st introId toStatus = - liftIO . withTransaction st $ \db -> - DB.execute - db - [sql| - UPDATE conn_intros - SET to_status = ? - WHERE intro_id = ?; - |] - (toStatus, introId) - - setIntroReStatus :: SQLiteStore -> IntroId -> IntroStatus -> m () - setIntroReStatus st introId reStatus = - liftIO . withTransaction st $ \db -> - DB.execute - db - [sql| - UPDATE conn_intros - SET re_status = ? - WHERE intro_id = ?; - |] - (reStatus, introId) - - createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId - createInvitation st gVar NewInvitation {viaConn, externalIntroId, connInfo, qInfo} = - liftIOEither . withTransaction st $ \db -> - createWithRandomId gVar $ \invId -> - DB.execute - db - [sql| - INSERT INTO conn_invitations - (inv_id, via_conn, external_intro_id, conn_info, queue_info) VALUES (?, ?, ?, ?, ?); - |] - (invId, viaConn, externalIntroId, connInfo, qInfo) - - getInvitation :: SQLiteStore -> InvitationId -> m Invitation - getInvitation st invId = - liftIOEither . withConnection st $ \db -> - invitation - <$> DB.query - db - [sql| - SELECT via_conn, external_intro_id, conn_info, queue_info, conn_id, status - FROM conn_invitations - WHERE inv_id = ?; - |] - (Only invId) - where - invitation [(viaConn, externalIntroId, connInfo, qInfo, connId, status)] = - Right $ Invitation {invId, viaConn, externalIntroId, connInfo, qInfo, connId, status} - invitation _ = Left SEInvitationNotFound - - addInvitationConn :: SQLiteStore -> InvitationId -> ConnId -> m () - addInvitationConn st invId connId = - liftIO . withTransaction st $ \db -> - DB.executeNamed - db - [sql| - UPDATE conn_invitations - SET conn_id = :conn_id, status = :status - WHERE inv_id = :inv_id; - |] - [":conn_id" := connId, ":status" := InvAcpt, ":inv_id" := invId] - - getConnInvitation :: SQLiteStore -> ConnId -> m (Maybe (Invitation, Connection 'CDuplex)) - getConnInvitation st cId = - liftIO . withTransaction st $ \db -> - DB.query - db - [sql| - SELECT inv_id, via_conn, external_intro_id, conn_info, queue_info, status - FROM conn_invitations - WHERE conn_id = ?; - |] - (Only cId) - >>= fmap join . traverse (getViaConn db) . invitation - where - invitation [(invId, viaConn, externalIntroId, connInfo, qInfo, status)] = - Just $ Invitation {invId, viaConn, externalIntroId, connInfo, qInfo, connId = Just cId, status} - invitation _ = Nothing - getViaConn :: DB.Connection -> Invitation -> IO (Maybe (Invitation, Connection 'CDuplex)) - getViaConn db inv@Invitation {viaConn} = fmap (inv,) . duplexConn <$> getConn_ db viaConn - duplexConn :: Either StoreError SomeConn -> Maybe (Connection 'CDuplex) - duplexConn (Right (SomeConn SCDuplex conn)) = Just conn - duplexConn _ = Nothing - - setInvitationStatus :: SQLiteStore -> InvitationId -> InvitationStatus -> m () - setInvitationStatus st invId status = - liftIO . withTransaction st $ \db -> - DB.execute - db - [sql| - UPDATE conn_invitations - SET status = ? WHERE inv_id = ?; - |] - (status, invId) - -- * Auxiliary helpers -- ? replace with ToField? - it's easy to forget to use this @@ -574,14 +429,6 @@ instance ToField MsgIntegrity where toField = toField . serializeMsgIntegrity instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP -instance ToField IntroStatus where toField = toField . serializeIntroStatus - -instance FromField IntroStatus where fromField = fromTextField_ introStatusT - -instance ToField InvitationStatus where toField = toField . serializeInvStatus - -instance FromField InvitationStatus where fromField = fromTextField_ invStatusT - instance ToField SMPQueueInfo where toField = toField . serializeSmpQueueInfo instance FromField SMPQueueInfo where fromField = blobFieldParser smpQueueInfoP @@ -646,22 +493,20 @@ insertRcvQueue_ dbConn connId RcvQueue {..} = do ] insertRcvConnection_ :: DB.Connection -> ConnData -> RcvQueue -> IO () -insertRcvConnection_ dbConn ConnData {connId, viaInv, connLevel} RcvQueue {server, rcvId} = do +insertRcvConnection_ dbConn ConnData {connId} RcvQueue {server, rcvId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn [sql| INSERT INTO connections - ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, via_inv, conn_level, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) + ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) VALUES - (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, :via_inv,:conn_level, 0, 0, 0, 0, x'', x''); + (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, 0, 0, 0, 0, x'', x''); |] [ ":conn_alias" := connId, ":rcv_host" := host server, ":rcv_port" := port_, - ":rcv_id" := rcvId, - ":via_inv" := viaInv, - ":conn_level" := connLevel + ":rcv_id" := rcvId ] -- * createSndConn helpers @@ -688,22 +533,20 @@ insertSndQueue_ dbConn connId SndQueue {..} = do ] insertSndConnection_ :: DB.Connection -> ConnData -> SndQueue -> IO () -insertSndConnection_ dbConn ConnData {connId, viaInv, connLevel} SndQueue {server, sndId} = do +insertSndConnection_ dbConn ConnData {connId} SndQueue {server, sndId} = do let port_ = serializePort_ $ port server DB.executeNamed dbConn [sql| INSERT INTO connections - ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, via_inv, conn_level, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) + ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) VALUES - (:conn_alias, NULL, NULL, NULL, :snd_host,:snd_port,:snd_id,:via_inv,:conn_level, 0, 0, 0, 0, x'', x''); + (:conn_alias, NULL, NULL, NULL, :snd_host,:snd_port,:snd_id, 0, 0, 0, 0, x'', x''); |] [ ":conn_alias" := connId, ":snd_host" := host server, ":snd_port" := port_, - ":snd_id" := sndId, - ":via_inv" := viaInv, - ":conn_level" := connLevel + ":snd_id" := sndId ] -- * getConn helpers @@ -722,11 +565,11 @@ getConn_ dbConn connId = _ -> Left SEConnNotFound getConnData_ :: DB.Connection -> ConnId -> IO (Maybe ConnData) -getConnData_ dbConn connId = +getConnData_ dbConn connId' = connData - <$> DB.query dbConn "SELECT via_inv, conn_level FROM connections WHERE conn_alias = ?;" (Only connId) + <$> DB.query dbConn "SELECT conn_alias FROM connections WHERE conn_alias = ?;" (Only connId') where - connData [(viaInv, connLevel)] = Just ConnData {connId, viaInv, connLevel} + connData [Only connId] = Just ConnData {connId} connData _ = Nothing getRcvQueueByConnAlias_ :: DB.Connection -> ConnId -> IO (Maybe RcvQueue) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index e8faf5ea9..59b4ceb49 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -45,11 +45,6 @@ agentTests (ATransport t) = do smpAgentTest3_1_1 $ testSubscription t it "should send notifications to client when server disconnects" $ smpAgentServerTest $ testSubscrNotification t - describe "Introduction" do - it "should send and accept introduction" $ - smpAgentTest3 $ testIntroduction t - it "should send and accept introduction (random IDs)" $ - smpAgentTest3 $ testIntroductionRandomIds t -- | receive message to handle `h` (<#:) :: Transport c => c -> IO (ATransmissionOrError 'Agent) @@ -99,8 +94,8 @@ testDuplexConnection _ alice bob = do ("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW") let qInfo' = serializeSmpQueueInfo qInfo bob #: ("11", "alice", "JOIN " <> qInfo' <> " 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", Right (REQ confId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "ACPT " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) bob <# ("", "alice", INFO "alice's connInfo") bob <# ("", "alice", CON) alice <# ("", "bob", CON) @@ -122,9 +117,9 @@ testDuplexConnRandomIds _ alice bob = do ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> qInfo' <> " 14\nbob's connInfo") - ("", bobConn', Right (CONF confId "bob's connInfo")) <- (alice <#:) + ("", bobConn', Right (REQ confId "bob's connInfo")) <- (alice <#:) bobConn' `shouldBe` bobConn - alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False + alice #: ("2", bobConn, "ACPT " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) @@ -161,62 +156,13 @@ testSubscrNotification _ (server, _) client = do killThread server client <# ("", "conn1", END) -testIntroduction :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () -testIntroduction _ alice bob tom = do - -- establish connections - (alice, "alice") `connect` (bob, "bob") - (alice, "alice") `connect` (tom, "tom") - -- send introduction of tom to bob - alice #: ("1", "bob", "INTRO tom 8\nmeet tom") #> ("1", "bob", OK) - ("", "alice", Right (REQ invId1 "meet tom")) <- (bob <#:) - bob #: ("2", "tom_via_alice", "ACPT " <> invId1 <> " 7\nI'm bob") #> ("2", "tom_via_alice", OK) - ("", "alice", Right (REQ invId2 "I'm bob")) <- (tom <#:) - tom #: ("3", "bob_via_alice", "ACPT " <> invId2 <> " 8\ntom here") #> ("3", "bob_via_alice", OK) - ("", "tom_via_alice", Right (CONF confId "tom here")) <- (bob <#:) - bob #: ("3.1", "tom_via_alice", "LET " <> confId <> " 7\nI'm bob") #> ("3.1", "tom_via_alice", OK) - bob <# ("", "tom_via_alice", CON) - tom <# ("", "bob_via_alice", INFO "I'm bob") - tom <# ("", "bob_via_alice", CON) - alice <# ("", "bob", ICON "tom") - -- they can message each other now - tom #: ("4", "bob_via_alice", "SEND :hello") #> ("4", "bob_via_alice", SENT 1) - bob <#= \case ("", "tom_via_alice", Msg "hello") -> True; _ -> False - bob #: ("5", "tom_via_alice", "SEND 9\nhello too") #> ("5", "tom_via_alice", SENT 2) - tom <#= \case ("", "bob_via_alice", Msg "hello too") -> True; _ -> False - -testIntroductionRandomIds :: forall c. Transport c => TProxy c -> c -> c -> c -> IO () -testIntroductionRandomIds _ alice bob tom = do - -- establish connections - (aliceB, bobA) <- alice `connect'` bob - (aliceT, tomA) <- alice `connect'` tom - -- send introduction of tom to bob - alice #: ("1", bobA, "INTRO " <> tomA <> " 8\nmeet tom") #> ("1", bobA, OK) - ("", aliceB', Right (REQ invId1 "meet tom")) <- (bob <#:) - aliceB' `shouldBe` aliceB - ("2", tomB, Right OK) <- bob #: ("2", "", "ACPT " <> invId1 <> " 7\nI'm bob") - ("", aliceT', Right (REQ invId2 "I'm bob")) <- (tom <#:) - aliceT' `shouldBe` aliceT - ("3", bobT, Right OK) <- tom #: ("3", "", "ACPT " <> invId2 <> " 8\ntom here") - ("", tomB', Right (CONF confId "tom here")) <- (bob <#:) - tomB' `shouldBe` tomB - bob #: ("3.1", tomB, "LET " <> confId <> " 7\nI'm bob") =#> \case ("3.1", c, OK) -> c == tomB; _ -> False - bob <# ("", tomB, CON) - tom <# ("", bobT, INFO "I'm bob") - tom <# ("", bobT, CON) - alice <# ("", bobA, ICON tomA) - -- they can message each other now - tom #: ("4", bobT, "SEND :hello") #> ("4", bobT, SENT 1) - bob <#= \case ("", c, Msg "hello") -> c == tomB; _ -> False - bob #: ("5", tomB, "SEND 9\nhello too") #> ("5", tomB, SENT 2) - tom <#= \case ("", c, Msg "hello too") -> c == bobT; _ -> False - connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") let qInfo' = serializeSmpQueueInfo qInfo h2 #: ("c2", name1, "JOIN " <> qInfo' <> " 5\ninfo2") #> ("c2", name1, OK) - ("", _, Right (CONF connId "info2")) <- (h1 <#:) - h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) + ("", _, Right (REQ connId "info2")) <- (h1 <#:) + h1 #: ("c3", name2, "ACPT " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) h2 <# ("", name1, INFO "info1") h2 <# ("", name1, CON) h1 <# ("", name2, CON) @@ -226,8 +172,8 @@ connect' h1 h2 = do ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") let qInfo' = serializeSmpQueueInfo qInfo ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo' <> " 5\ninfo2") - ("", _, Right (CONF connId "info2")) <- (h1 <#:) - h1 #: ("c3", conn2, "LET " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False + ("", _, Right (REQ connId "info2")) <- (h1 <#:) + h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False h2 <# ("", conn1, INFO "info1") h2 <# ("", conn1, CON) h1 <# ("", conn2, CON) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index e2a25926e..4d945553d 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -56,8 +56,8 @@ testAgentClient = do Right () <- runExceptT $ do (bobId, qInfo) <- createConnection alice aliceId <- joinConnection bob qInfo "bob's connInfo" - ("", _, CONF confId "bob's connInfo") <- get alice - allowConnection alice bobId confId "alice's connInfo" + ("", _, REQ confId "bob's connInfo") <- get alice + acceptConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) @@ -93,8 +93,8 @@ testAsyncInitiatingOffline = do aliceId <- joinConnection bob qInfo "bob's connInfo" alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, CONF confId "bob's connInfo") <- get alice' - allowConnection alice' bobId confId "alice's connInfo" + ("", _, REQ confId "bob's connInfo") <- get alice' + acceptConnection alice' bobId confId "alice's connInfo" get alice' ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) @@ -109,8 +109,8 @@ testAsyncJoiningOfflineBeforeActivation = do (bobId, qInfo) <- createConnection alice aliceId <- joinConnection bob qInfo "bob's connInfo" disconnectAgentClient bob - ("", _, CONF confId "bob's connInfo") <- get alice - allowConnection alice bobId confId "alice's connInfo" + ("", _, REQ confId "bob's connInfo") <- get alice + acceptConnection alice bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId get alice ##> ("", bobId, CON) @@ -133,8 +133,8 @@ testAsyncBothOffline = do disconnectAgentClient bob alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, CONF confId "bob's connInfo") <- get alice' - allowConnection alice' bobId confId "alice's connInfo" + ("", _, REQ confId "bob's connInfo") <- get alice' + acceptConnection alice' bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId get alice' ##> ("", bobId, CON) diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 0f364c969..f5c64d923 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -147,7 +147,7 @@ testForeignKeysEnabled = `shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint) cData1 :: ConnData -cData1 = ConnData {connId = "conn1", viaInv = Nothing, connLevel = 1} +cData1 = ConnData {connId = "conn1"} rcvQueue1 :: RcvQueue rcvQueue1 = From d9084522afa939b2993d4c3e94db90cf145ffadf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 5 Aug 2021 08:33:48 +0100 Subject: [PATCH 15/29] agent lock to avoid running subscriber and client processing in parallel; remove check that the connection is active when delivering a message (#176) * agent lock to avoid running subscriber and client processing in parallel; remove check that the connection is active when delivering a message * use agent lock in agent functions --- src/Simplex/Messaging/Agent.hs | 46 ++++++++++++--------- src/Simplex/Messaging/Agent/Client.hs | 6 ++- src/Simplex/Messaging/Agent/Store/SQLite.hs | 4 +- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index d66d2184e..e599d8074 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -122,31 +122,34 @@ type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) -- | Create SMP agent connection (NEW command) createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection c = (`runReaderT` agentEnv c) $ newConn c "" +createConnection c = withAgentClient c $ newConn c "" -- | Join SMP agent connection (JOIN command) joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection c = (`runReaderT` agentEnv c) .: joinConn c "" +joinConnection c = withAgentClient c .: joinConn c "" -- | Approve confirmation (LET command) acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -acceptConnection c = (`runReaderT` agentEnv c) .:. acceptConnection' c +acceptConnection c = withAgentClient c .:. acceptConnection' c -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -subscribeConnection c = (`runReaderT` agentEnv c) . subscribeConnection' c +subscribeConnection c = withAgentClient c . subscribeConnection' c -- | Send message to the connection (SEND command) sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId -sendMessage c = (`runReaderT` agentEnv c) .: sendMessage' c +sendMessage c = withAgentClient c .: sendMessage' c -- | Suspend SMP agent connection (OFF command) suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -suspendConnection c = (`runReaderT` agentEnv c) . suspendConnection' c +suspendConnection c = withAgentClient c . suspendConnection' c -- | Delete SMP agent connection (DEL command) deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -deleteConnection c = (`runReaderT` agentEnv c) . deleteConnection' c +deleteConnection c = withAgentClient c . deleteConnection' c + +withAgentClient :: AgentErrorMonad m => AgentClient -> ReaderT Env m a -> m a +withAgentClient c = withAgentLock c . (`runReaderT` agentEnv c) -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient @@ -186,10 +189,16 @@ logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> logClient AgentClient {clientId} dir (corrId, connId, cmd) = do logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd] +withAgentLock :: MonadUnliftIO m => AgentClient -> m a -> m a +withAgentLock AgentClient {lock} = + E.bracket_ + (void . atomically $ takeTMVar lock) + (atomically $ putTMVar lock ()) + client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () client c@AgentClient {rcvQ, subQ} = forever $ do (corrId, connId, cmd) <- atomically $ readTBQueue rcvQ - runExceptT (processCommand c (connId, cmd)) + withAgentLock c (runExceptT $ processCommand c (connId, cmd)) >>= atomically . writeTBQueue subQ . \case Left e -> (corrId, connId, ERR e) Right (connId', resp) -> (corrId, connId', resp) @@ -380,7 +389,7 @@ sendControlMessage c sq agentMessage = do subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ - runExceptT (processSMPTransmission c t) >>= \case + withAgentLock c (runExceptT $ processSMPTransmission c t) >>= \case Left e -> liftIO $ print e Right _ -> return () @@ -467,17 +476,14 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () agentClientMsg externalPrevSndHash sender broker msgBody internalHash = do logServer "<--" c srv rId "MSG " - case status of - Active -> do - internalTs <- liftIO getCurrentTime - (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId) - let integrity = checkMsgIntegrity prevExtSndId (fst sender) prevRcvMsgHash externalPrevSndHash - recipient = (unId internalId, internalTs) - msgMeta = MsgMeta {integrity, recipient, sender, broker} - rcvMsg = RcvMsgData {msgMeta, msgBody, internalRcvId, internalHash, externalPrevSndHash} - withStore $ \st -> createRcvMsg st connId rcvMsg - notify $ MSG msgMeta msgBody - _ -> prohibited + internalTs <- liftIO getCurrentTime + (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId) + let integrity = checkMsgIntegrity prevExtSndId (fst sender) prevRcvMsgHash externalPrevSndHash + recipient = (unId internalId, internalTs) + msgMeta = MsgMeta {integrity, recipient, sender, broker} + rcvMsg = RcvMsgData {msgMeta, msgBody, internalRcvId, internalHash, externalPrevSndHash} + withStore $ \st -> createRcvMsg st connId rcvMsg + notify $ MSG msgMeta msgBody checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash receivedPrevMsgHash diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 9ffbd44e0..3f72ebc21 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -73,7 +73,8 @@ data AgentClient = AgentClient activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress clientId :: Int, agentEnv :: Env, - smpSubscriber :: Async () + smpSubscriber :: Async (), + lock :: TMVar () } newAgentClient :: Env -> STM AgentClient @@ -87,7 +88,8 @@ newAgentClient agentEnv = do subscrConns <- newTVar M.empty activations <- newTVar M.empty clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, clientId, agentEnv, smpSubscriber = undefined} + lock <- newTMVar () + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, clientId, agentEnv, smpSubscriber = undefined, lock} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index f6d2e2fd9..714501a0c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -186,7 +186,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto getAllConnIds :: SQLiteStore -> m [ConnId] getAllConnIds st = - liftIO . withConnection st $ \db -> + liftIO . withTransaction st $ \db -> concat <$> (DB.query_ db "SELECT conn_alias FROM connections;" :: IO [[ConnId]]) getRcvConn :: SQLiteStore -> SMPServer -> SMP.RecipientId -> m SomeConn @@ -334,7 +334,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto getAcceptedConfirmation :: SQLiteStore -> ConnId -> m AcceptedConfirmation getAcceptedConfirmation st connId = - liftIOEither . withConnection st $ \db -> + liftIOEither . withTransaction st $ \db -> confirmation <$> DB.query db From e045774caa057a6d40ef9735f30e0af958711859 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 9 Aug 2021 08:49:49 +0100 Subject: [PATCH 16/29] reconnect server and resubscribe connections after disconnection (#178) --- simplexmq.cabal | 3 +- src/Simplex/Messaging/Agent.hs | 6 -- src/Simplex/Messaging/Agent/Client.hs | 103 ++++++++++++------- src/Simplex/Messaging/Agent/Env/SQLite.hs | 16 +-- src/Simplex/Messaging/Agent/Protocol.hs | 10 ++ src/Simplex/Messaging/Agent/RetryInterval.hs | 28 +++++ src/Simplex/Messaging/Agent/Store.hs | 6 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 2 +- tests/AgentTests.hs | 7 +- tests/SMPAgentClient.hs | 1 + 10 files changed, 124 insertions(+), 58 deletions(-) create mode 100644 src/Simplex/Messaging/Agent/RetryInterval.hs diff --git a/simplexmq.cabal b/simplexmq.cabal index 0c39fd79b..c34c18ef6 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 576ad28116836a490d6974cd6322169dd38b5df984c16fe296387427b5bef3d5 +-- hash: 5169db4a4922766c79f08cbdb91d4c765520372273ab432569eba25a253a8dbb name: simplexmq version: 0.3.2 @@ -35,6 +35,7 @@ library Simplex.Messaging.Agent.Client Simplex.Messaging.Agent.Env.SQLite Simplex.Messaging.Agent.Protocol + Simplex.Messaging.Agent.RetryInterval Simplex.Messaging.Agent.Store Simplex.Messaging.Agent.Store.SQLite Simplex.Messaging.Agent.Store.SQLite.Migrations diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index e599d8074..6d3747497 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -189,12 +189,6 @@ logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> logClient AgentClient {clientId} dir (corrId, connId, cmd) = do logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd] -withAgentLock :: MonadUnliftIO m => AgentClient -> m a -> m a -withAgentLock AgentClient {lock} = - E.bracket_ - (void . atomically $ takeTMVar lock) - (atomically $ putTMVar lock ()) - client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () client c@AgentClient {rcvQ, subQ} = forever $ do (corrId, connId, cmd) <- atomically $ readTBQueue rcvQ diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 3f72ebc21..27bf21a41 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -11,7 +11,7 @@ module Simplex.Messaging.Agent.Client ( AgentClient (..), newAgentClient, AgentMonad, - getSMPServerClient, + withAgentLock, closeAgentClient, newRcvQueue, subscribeQueue, @@ -35,7 +35,7 @@ module Simplex.Messaging.Agent.Client ) where -import Control.Concurrent.Async (Async, uninterruptibleCancel) +import Control.Concurrent.Async (Async, async, uninterruptibleCancel) import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except @@ -47,18 +47,19 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Maybe (isNothing) import Data.Set (Set) import qualified Data.Set as S import Data.Text.Encoding import Data.Time.Clock import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, SenderPublicKey) import Simplex.Messaging.Util (bshow, liftEitherError, liftError) -import UnliftIO.Concurrent import UnliftIO.Exception (IOException) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -68,9 +69,10 @@ data AgentClient = AgentClient subQ :: TBQueue (ATransmission 'Agent), msgQ :: TBQueue SMPServerTransmission, smpClients :: TVar (Map SMPServer SMPClient), - subscrSrvrs :: TVar (Map SMPServer (Set ConnId)), + subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)), subscrConns :: TVar (Map ConnId SMPServer), activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress + reconnections :: TVar [Async ()], clientId :: Int, agentEnv :: Env, smpSubscriber :: Async (), @@ -87,9 +89,10 @@ newAgentClient agentEnv = do subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty activations <- newTVar M.empty + reconnections <- newTVar [] clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) lock <- newTMVar () - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, clientId, agentEnv, smpSubscriber = undefined, lock} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) @@ -109,41 +112,74 @@ getSMPServerClient c@AgentClient {smpClients, msgQ} srv = connectClient :: m SMPClient connectClient = do cfg <- asks $ smpCfg . config - liftEitherError smpClientError (getSMPClient srv cfg msgQ clientDisconnected) + u <- askUnliftIO + liftEitherError smpClientError (getSMPClient srv cfg msgQ $ clientDisconnected u) `E.catch` internalError where internalError :: IOException -> m SMPClient internalError = throwError . INTERNAL . show - clientDisconnected :: IO () - clientDisconnected = do - removeSubs >>= mapM_ (mapM_ notifySub) + clientDisconnected :: UnliftIO m -> IO () + clientDisconnected u = do + removeClientSubs >>= (`forM_` serverDown u) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeSubs :: IO (Maybe (Set ConnId)) - removeSubs = atomically $ do + removeClientSubs :: IO (Maybe (Map ConnId RcvQueue)) + removeClientSubs = atomically $ do modifyTVar smpClients $ M.delete srv cs <- M.lookup srv <$> readTVar (subscrSrvrs c) modifyTVar (subscrSrvrs c) $ M.delete srv - modifyTVar (subscrConns c) $ maybe id deleteKeys cs + modifyTVar (subscrConns c) $ maybe id (deleteKeys . M.keysSet) cs return cs where deleteKeys :: Ord k => Set k -> Map k a -> Map k a deleteKeys ks m = S.foldr' M.delete m ks - notifySub :: ConnId -> IO () - notifySub connId = atomically $ writeTBQueue (subQ c) ("", connId, END) + serverDown :: UnliftIO m -> Map ConnId RcvQueue -> IO () + serverDown u cs = unless (M.null cs) $ do + mapM_ (notifySub DOWN) $ M.keysSet cs + a <- async . unliftIO u $ tryReconnectClient cs + atomically $ modifyTVar (reconnections c) (a :) + + tryReconnectClient :: Map ConnId RcvQueue -> m () + tryReconnectClient cs = do + ri <- asks $ reconnectInterval . config + withRetryInterval ri $ \loop -> + reconnectClient cs `catchError` const loop + + reconnectClient :: Map ConnId RcvQueue -> m () + reconnectClient cs = do + withAgentLock c . withSMP c srv $ \smp -> do + subs <- readTVarIO $ subscrConns c + forM_ (M.toList cs) $ \(connId, rq@RcvQueue {rcvPrivateKey, rcvId}) -> + when (isNothing $ M.lookup connId subs) $ do + subscribeSMPQueue smp rcvPrivateKey rcvId + `catchError` \case + SMPServerError e -> liftIO $ notifySub (ERR $ SMP e) connId + e -> throwError e + addSubscription c rq connId + liftIO $ notifySub UP connId + + notifySub :: ACommand 'Agent -> ConnId -> IO () + notifySub cmd connId = atomically $ writeTBQueue (subQ c) ("", connId, cmd) closeAgentClient :: MonadUnliftIO m => AgentClient -> m () closeAgentClient c = liftIO $ do closeSMPServerClients c - cancelActivations c + cancelActions $ activations c + cancelActions $ reconnections c closeSMPServerClients :: AgentClient -> IO () closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ closeSMPClient -cancelActivations :: AgentClient -> IO () -cancelActivations c = readTVarIO (activations c) >>= mapM_ uninterruptibleCancel +cancelActions :: Foldable f => TVar (f (Async ())) -> IO () +cancelActions as = readTVarIO as >>= mapM_ uninterruptibleCancel + +withAgentLock :: MonadUnliftIO m => AgentClient -> m a -> m a +withAgentLock AgentClient {lock} = + E.bracket_ + (void . atomically $ takeTMVar lock) + (atomically $ putTMVar lock ()) withSMP_ :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> m a) -> m a withSMP_ c srv action = @@ -207,13 +243,13 @@ subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do addSubscription c rq connId addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m () -addSubscription c RcvQueue {server} connId = atomically $ do +addSubscription c rq@RcvQueue {server} connId = atomically $ do modifyTVar (subscrConns c) $ M.insert connId server modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server where - addSub :: Maybe (Set ConnId) -> Set ConnId - addSub (Just cs) = S.insert connId cs - addSub _ = S.singleton connId + addSub :: Maybe (Map ConnId RcvQueue) -> Map ConnId RcvQueue + addSub (Just cs) = M.insert connId rq cs + addSub _ = M.singleton connId rq removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m () removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ do @@ -223,10 +259,10 @@ removeSubscription AgentClient {subscrConns, subscrSrvrs} connId = atomically $ (modifyTVar subscrSrvrs . M.alter (>>= delSub)) (M.lookup connId cs) where - delSub :: Set ConnId -> Maybe (Set ConnId) + delSub :: Map ConnId RcvQueue -> Maybe (Map ConnId RcvQueue) delSub cs = - let cs' = S.delete connId cs - in if S.null cs' then Nothing else Just cs' + let cs' = M.delete connId cs + in if M.null cs' then Nothing else Just cs' addActivation :: MonadUnliftIO m => AgentClient -> ConnId -> Async () -> m () addActivation c connId a = atomically . modifyTVar (activations c) $ M.insert connId a @@ -257,10 +293,13 @@ sendConfirmation c sq@SndQueue {server, sndId} senderKey cInfo = mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey cInfo sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m () -sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey RetryInterval {initialInterval, increaseAfter, maxInterval} = +sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey ri = withLogSMP_ c server sndId "SEND (retrying)" $ \smp -> do msg <- mkHello smp $ AckMode On - liftSMP $ send 0 initialInterval msg smp + liftSMP . withRetryInterval ri $ \loop -> + sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case + SMPServerError AUTH -> loop + e -> throwE e where mkHello :: SMPClient -> AckMode -> m ByteString mkHello smp ackMode = do @@ -273,18 +312,6 @@ sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey RetryInterval { agentMessage = HELLO verifyKey ackMode } - send :: Int -> Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO () - send elapsedTime delay msg smp = - sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case - SMPServerError AUTH -> do - threadDelay delay - let newDelay = - if elapsedTime < increaseAfter || delay == maxInterval - then delay - else min (delay * 3 `div` 2) maxInterval - send (elapsedTime + delay) newDelay msg smp - e -> throwE e - secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey = withLogSMP c server rcvId "KEY " $ \smp -> diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 85877aa0d..6a063d4dd 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -12,6 +12,7 @@ import Data.List.NonEmpty (NonEmpty) import Network.Socket import Numeric.Natural import Simplex.Messaging.Agent.Protocol (SMPServer) +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client @@ -27,18 +28,13 @@ data AgentConfig = AgentConfig dbFile :: FilePath, dbPoolSize :: Int, smpCfg :: SMPClientConfig, - retryInterval :: RetryInterval + retryInterval :: RetryInterval, + reconnectInterval :: RetryInterval } minute :: Int minute = 60_000_000 -data RetryInterval = RetryInterval - { initialInterval :: Int, - increaseAfter :: Int, - maxInterval :: Int - } - defaultAgentConfig :: AgentConfig defaultAgentConfig = AgentConfig @@ -55,6 +51,12 @@ defaultAgentConfig = { initialInterval = 1_000_000, increaseAfter = minute, maxInterval = 10 * minute + }, + reconnectInterval = + RetryInterval + { initialInterval = 1_000_000, + increaseAfter = 10_000_000, + maxInterval = 10_000_000 } } diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index b8f29e856..5b0dbc485 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -162,9 +162,12 @@ data ACommand (p :: AParty) where CON :: ACommand Agent -- notification that connection is established SUB :: ACommand Client END :: ACommand Agent + DOWN :: ACommand Agent + UP :: ACommand Agent -- QST :: QueueDirection -> ACommand Client -- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent SEND :: MsgBody -> ACommand Client + MID :: AgentMsgId -> ACommand Agent SENT :: AgentMsgId -> ACommand Agent MSG :: MsgMeta -> MsgBody -> ACommand Agent -- ACK :: AgentMsgId -> ACommand Client @@ -455,7 +458,10 @@ commandP = <|> "INFO " *> infoCmd <|> "SUB" $> ACmd SClient SUB <|> "END" $> ACmd SAgent END + <|> "DOWN" $> ACmd SAgent DOWN + <|> "UP" $> ACmd SAgent UP <|> "SEND " *> sendCmd + <|> "MID " *> msgIdResp <|> "SENT " *> sentResp <|> "MSG " *> message <|> "OFF" $> ACmd SClient OFF @@ -470,6 +476,7 @@ commandP = acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString + msgIdResp = ACmd SAgent . MID <$> A.decimal sentResp = ACmd SAgent . SENT <$> A.decimal message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString) msgMetaP = do @@ -505,7 +512,10 @@ serializeCommand = \case INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" + DOWN -> "DOWN" + UP -> "UP" SEND msgBody -> "SEND " <> serializeBinary msgBody + MID mId -> "MID " <> bshow mId SENT mId -> "SENT " <> bshow mId MSG msgMeta msgBody -> "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeBinary msgBody diff --git a/src/Simplex/Messaging/Agent/RetryInterval.hs b/src/Simplex/Messaging/Agent/RetryInterval.hs new file mode 100644 index 000000000..048b9e09c --- /dev/null +++ b/src/Simplex/Messaging/Agent/RetryInterval.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Simplex.Messaging.Agent.RetryInterval where + +import Control.Concurrent (threadDelay) +import Control.Monad.IO.Class (MonadIO, liftIO) + +data RetryInterval = RetryInterval + { initialInterval :: Int, + increaseAfter :: Int, + maxInterval :: Int + } + +withRetryInterval :: forall m. MonadIO m => RetryInterval -> (m () -> m ()) -> m () +withRetryInterval RetryInterval {initialInterval, increaseAfter, maxInterval} action = + callAction 0 initialInterval + where + callAction :: Int -> Int -> m () + callAction elapsedTime delay = action loop + where + loop = do + let newDelay = + if elapsedTime < increaseAfter || delay == maxInterval + then delay + else min (delay * 3 `div` 2) maxInterval + liftIO $ threadDelay delay + callAction (elapsedTime + delay) newDelay diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 72f157397..c7fb05b40 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -264,9 +264,9 @@ data SndMsg = SndMsg newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show) data SndMsgStatus - = Created - | Sent - | Delivered + = SndMsgCreated + | SndMsgSent + | SndMsgDelivered deriving (Eq, Show) type SentTs = UTCTime diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 714501a0c..52d3a84ae 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -795,7 +795,7 @@ insertSndMsgDetails_ dbConn connId SndMsgData {..} = [ ":conn_alias" := connId, ":internal_snd_id" := internalSndId, ":internal_id" := internalId, - ":snd_status" := Created, + ":snd_status" := SndMsgCreated, ":internal_hash" := internalHash ] diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 59b4ceb49..aa579a175 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -16,6 +16,7 @@ import Control.Concurrent import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient +import SMPClient (withSmpServer) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) @@ -150,11 +151,13 @@ testSubscription _ alice1 alice2 bob = do alice1 #:# "nothing else should be delivered to alice1" testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () -testSubscrNotification _ (server, _) client = do +testSubscrNotification t (server, _) client = do client #: ("1", "conn1", "NEW") =#> \case ("1", "conn1", INV {}) -> True; _ -> False client #:# "nothing should be delivered to client before the server is killed" killThread server - client <# ("", "conn1", END) + client <# ("", "conn1", DOWN) + withSmpServer (ATransport t) $ + client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index af3093ca0..af1d81b7e 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -24,6 +24,7 @@ import SMPClient import Simplex.Messaging.Agent (runSMPAgentBlocking) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig) import Simplex.Messaging.Transport import Test.Hspec From dd5137c336d5525c38b068d7212964b4ab196a33 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 12 Aug 2021 22:15:55 +0100 Subject: [PATCH 17/29] asynchronous message sending to allow accepting messages when agent (or server) is offline (#179) * asynchronous message sending to allow accepting messages when agent (or server) is offline * send pending messages after agent restart; test for pending messages delivery when server restarted * test pending message delivery after agent restart * message delivery process per server * test layout --- migrations/20210809_snd_messages.sql | 3 + src/Simplex/Messaging/Agent.hs | 154 +++++++++++++++----- src/Simplex/Messaging/Agent/Client.hs | 12 +- src/Simplex/Messaging/Agent/Protocol.hs | 4 + src/Simplex/Messaging/Agent/Store.hs | 14 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 69 +++++++-- tests/AgentTests.hs | 124 ++++++++++++---- tests/AgentTests/FunctionalAPITests.hs | 11 +- tests/AgentTests/SQLiteTests.hs | 3 +- tests/SMPAgentClient.hs | 32 +++- 10 files changed, 339 insertions(+), 87 deletions(-) create mode 100644 migrations/20210809_snd_messages.sql diff --git a/migrations/20210809_snd_messages.sql b/migrations/20210809_snd_messages.sql new file mode 100644 index 000000000..ef1624ec3 --- /dev/null +++ b/migrations/20210809_snd_messages.sql @@ -0,0 +1,3 @@ +ALTER TABLE messages ADD msg_body BLOB NOT NULL DEFAULT x''; -- this field replaces body TEXT +-- TODO possibly migrate the data from body if it is possible in migration +ALTER TABLE snd_messages ADD previous_msg_hash BLOB NOT NULL DEFAULT x''; diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 6d3747497..cf875b09f 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -62,6 +62,9 @@ import Data.Composition ((.:), (.:.)) import Data.Functor (($>)) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (isJust) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8) import Data.Time.Clock @@ -69,6 +72,7 @@ import Database.SQLite.Simple (SQLError) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Client (SMPServerTransmission) @@ -78,7 +82,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) import Simplex.Messaging.Util (bshow) import System.Random (randomR) -import UnliftIO.Async (async, race_) +import UnliftIO.Async (Async, async, race_) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -226,7 +230,7 @@ processCommand c (connId, cmd) = case cmd of JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) - SEND msgBody -> (connId,) . SENT . unId <$> sendMessage' c connId msgBody + SEND msgBody -> (connId,) . MID . unId <$> sendMessage' c connId msgBody OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) @@ -282,22 +286,32 @@ processConfirmation c rq sndKey = do subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = withStore (`getConn` connId) >>= \case - SomeConn _ (DuplexConnection _ rq sq) -> case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \verifyKey -> do - conf <- withStore (`getAcceptedConfirmation` connId) - secureQueue c rq $ senderKey (conf :: AcceptedConfirmation) - withStore $ \st -> setRcvQueueStatus st rq Secured - activateSecuredQueue rq sq verifyKey - Secured -> withVerifyKey sq $ activateSecuredQueue rq sq - Active -> subscribeQueue c rq connId - _ -> throwError $ INTERNAL "unexpected queue status" - SomeConn _ (SndConnection _ sq) -> case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \verifyKey -> - activateQueueJoining c connId sq verifyKey =<< resumeInterval - Active -> throwError $ CONN SIMPLEX - _ -> throwError $ INTERNAL "unexpected queue status" + SomeConn _ (DuplexConnection _ rq sq) -> do + resumeDelivery sq + case status (sq :: SndQueue) of + Confirmed -> withVerifyKey sq $ \verifyKey -> do + conf <- withStore (`getAcceptedConfirmation` connId) + secureQueue c rq $ senderKey (conf :: AcceptedConfirmation) + withStore $ \st -> setRcvQueueStatus st rq Secured + activateSecuredQueue rq sq verifyKey + Secured -> withVerifyKey sq $ activateSecuredQueue rq sq + Active -> subscribeQueue c rq connId + _ -> throwError $ INTERNAL "unexpected queue status" + SomeConn _ (SndConnection _ sq) -> do + resumeDelivery sq + case status (sq :: SndQueue) of + Confirmed -> withVerifyKey sq $ \verifyKey -> + activateQueueJoining c connId sq verifyKey =<< resumeInterval + Active -> throwError $ CONN SIMPLEX + _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId where + resumeDelivery :: SndQueue -> m () + resumeDelivery SndQueue {server} = do + wasDelivering <- resumeMsgDelivery c connId server + unless wasDelivering $ do + pending <- withStore (`getPendingMsgs` connId) + queuePendingMsgs c connId pending withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m () withVerifyKey sq action = let err = throwError $ INTERNAL "missing signing key public counterpart" @@ -313,30 +327,96 @@ subscribeConnection' c connId = -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId -sendMessage' c connId msgBody = +sendMessage' c connId msg = withStore (`getConn` connId) >>= \case - SomeConn _ (DuplexConnection _ _ sq) -> sendMsg_ sq - SomeConn _ (SndConnection _ sq) -> sendMsg_ sq + SomeConn _ (DuplexConnection _ _ sq) -> enqueueMessage sq + SomeConn _ (SndConnection _ sq) -> enqueueMessage sq _ -> throwError $ CONN SIMPLEX where - sendMsg_ :: SndQueue -> m InternalId - sendMsg_ sq = do - internalTs <- liftIO getCurrentTime - (internalId, internalSndId, previousMsgHash) <- withStore (`updateSndIds` connId) - let msgStr = - serializeSMPMessage - SMPMessage - { senderMsgId = unSndId internalSndId, - senderTimestamp = internalTs, - previousMsgHash, - agentMessage = A_MSG msgBody - } - msgHash = C.sha256Hash msgStr - withStore $ \st -> - createSndMsg st connId $ - SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash} - sendAgentMessage c sq msgStr - pure internalId + enqueueMessage :: SndQueue -> m InternalId + enqueueMessage SndQueue {server} = do + msgId <- storeSentMsg + wasDelivering <- resumeMsgDelivery c connId server + pending <- + if wasDelivering + then pure [PendingMsg {connId, msgId}] + else withStore (`getPendingMsgs` connId) + queuePendingMsgs c connId pending + pure msgId + where + storeSentMsg :: m InternalId + storeSentMsg = do + internalTs <- liftIO getCurrentTime + withStore $ \st -> do + (internalId, internalSndId, previousMsgHash) <- updateSndIds st connId + let msgBody = + serializeSMPMessage + SMPMessage + { senderMsgId = unSndId internalSndId, + senderTimestamp = internalTs, + previousMsgHash, + agentMessage = A_MSG msg + } + msgData = SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = C.sha256Hash msgBody, previousMsgHash} + createSndMsg st connId msgData + pure internalId + +resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SMPServer -> m Bool +resumeMsgDelivery c connId srv = do + void $ resume srv (srvMsgDeliveries c) $ runSrvMsgDelivery c srv + resume connId (connMsgDeliveries c) $ runMsgDelivery c connId srv + where + resume :: Ord a => a -> TVar (Map a (Async ())) -> m () -> m Bool + resume key actionMap actionProcess = do + isDelivering <- isJust . M.lookup key <$> readTVarIO actionMap + unless isDelivering $ + async actionProcess + >>= atomically . modifyTVar actionMap . M.insert key + pure isDelivering + +queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> [PendingMsg] -> m () +queuePendingMsgs c connId pending = + atomically $ getPendingMsgQ connId (connMsgQueues c) >>= forM_ pending . writeTQueue + +getPendingMsgQ :: Ord a => a -> TVar (Map a (TQueue PendingMsg)) -> STM (TQueue PendingMsg) +getPendingMsgQ key queueMap = do + maybe newMsgQueue pure . M.lookup key =<< readTVar queueMap + where + newMsgQueue :: STM (TQueue PendingMsg) + newMsgQueue = do + mq <- newTQueue + modifyTVar queueMap $ M.insert key mq + pure mq + +runMsgDelivery :: AgentMonad m => AgentClient -> ConnId -> SMPServer -> m () +runMsgDelivery c connId srv = do + mq <- atomically . getPendingMsgQ connId $ connMsgQueues c + smq <- atomically . getPendingMsgQ srv $ srvMsgQueues c + forever . atomically $ readTQueue mq >>= writeTQueue smq + +runSrvMsgDelivery :: forall m. AgentMonad m => AgentClient -> SMPServer -> m () +runSrvMsgDelivery c@AgentClient {subQ} srv = do + mq <- atomically . getPendingMsgQ srv $ srvMsgQueues c + ri <- asks $ reconnectInterval . config + forever $ do + PendingMsg {connId, msgId} <- atomically $ readTQueue mq + let mId = unId msgId + r <- withStore $ \st -> + (Right <$> getPendingMsgData st connId msgId) + `E.catch` \(e :: E.SomeException) -> pure $ Left e + case r of + Left e -> notify connId $ MERR mId (INTERNAL $ show e) + Right (sq, msgBody) -> do + withRetryInterval ri $ \loop -> do + sendAgentMessage c sq msgBody + `catchError` \case + e@SMP {} -> notify connId $ MERR mId e + _ -> loop + notify connId $ SENT mId + withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent + where + notify :: ConnId -> ACommand 'Agent -> m () + notify connId cmd = atomically $ writeTBQueue subQ ("", connId, cmd) -- | Suspend SMP agent connection (OFF command) in Reader monad suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m () diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 27bf21a41..cc9cfb340 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -72,6 +72,10 @@ data AgentClient = AgentClient subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)), subscrConns :: TVar (Map ConnId SMPServer), activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress + connMsgQueues :: TVar (Map ConnId (TQueue PendingMsg)), + connMsgDeliveries :: TVar (Map ConnId (Async ())), + srvMsgQueues :: TVar (Map SMPServer (TQueue PendingMsg)), + srvMsgDeliveries :: TVar (Map SMPServer (Async ())), reconnections :: TVar [Async ()], clientId :: Int, agentEnv :: Env, @@ -89,10 +93,14 @@ newAgentClient agentEnv = do subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty activations <- newTVar M.empty + connMsgQueues <- newTVar M.empty + connMsgDeliveries <- newTVar M.empty + srvMsgQueues <- newTVar M.empty + srvMsgDeliveries <- newTVar M.empty reconnections <- newTVar [] clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) lock <- newTMVar () - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, connMsgQueues, connMsgDeliveries, srvMsgQueues, srvMsgDeliveries, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) @@ -168,6 +176,8 @@ closeAgentClient c = liftIO $ do closeSMPServerClients c cancelActions $ activations c cancelActions $ reconnections c + cancelActions $ connMsgDeliveries c + cancelActions $ srvMsgDeliveries c closeSMPServerClients :: AgentClient -> IO () closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ closeSMPClient diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 5b0dbc485..e24a91aa3 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -169,6 +169,7 @@ data ACommand (p :: AParty) where SEND :: MsgBody -> ACommand Client MID :: AgentMsgId -> ACommand Agent SENT :: AgentMsgId -> ACommand Agent + MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent MSG :: MsgMeta -> MsgBody -> ACommand Agent -- ACK :: AgentMsgId -> ACommand Client -- RCVD :: AgentMsgId -> ACommand Agent @@ -463,6 +464,7 @@ commandP = <|> "SEND " *> sendCmd <|> "MID " *> msgIdResp <|> "SENT " *> sentResp + <|> "MERR " *> msgErrResp <|> "MSG " *> message <|> "OFF" $> ACmd SClient OFF <|> "DEL" $> ACmd SClient DEL @@ -478,6 +480,7 @@ commandP = sendCmd = ACmd SClient . SEND <$> A.takeByteString msgIdResp = ACmd SAgent . MID <$> A.decimal sentResp = ACmd SAgent . SENT <$> A.decimal + msgErrResp = ACmd SAgent <$> (MERR <$> A.decimal <* A.space <*> agentErrorTypeP) message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString) msgMetaP = do integrity <- msgIntegrityP @@ -517,6 +520,7 @@ serializeCommand = \case SEND msgBody -> "SEND " <> serializeBinary msgBody MID mId -> "MID " <> bshow mId SENT mId -> "SENT " <> bshow mId + MERR mId e -> "MERR " <> bshow mId <> " " <> serializeAgentError e MSG msgMeta msgBody -> "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeBinary msgBody OFF -> "OFF" diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index c7fb05b40..c41110c32 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -56,6 +56,9 @@ class Monad m => MonadAgentStore s m where createRcvMsg :: s -> ConnId -> RcvMsgData -> m () updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) createSndMsg :: s -> ConnId -> SndMsgData -> m () + updateSndMsgStatus :: s -> ConnId -> InternalId -> SndMsgStatus -> m () + getPendingMsgData :: s -> ConnId -> InternalId -> m (SndQueue, MsgBody) + getPendingMsgs :: s -> ConnId -> m [PendingMsg] getMsg :: s -> ConnId -> InternalId -> m Msg -- * Queue types @@ -187,9 +190,16 @@ data SndMsgData = SndMsgData internalSndId :: InternalSndId, internalTs :: InternalTs, msgBody :: MsgBody, - internalHash :: MsgHash + internalHash :: MsgHash, + previousMsgHash :: MsgHash } +data PendingMsg = PendingMsg + { connId :: ConnId, + msgId :: InternalId + } + deriving (Show) + -- * Broadcast types type BroadcastId = ByteString @@ -310,6 +320,8 @@ data StoreError SEBadConnType ConnType | -- | Confirmation not found. SEConfirmationNotFound + | -- | Message not found + SEMsgNotFound | -- | Currently not used. The intention was to pass current expected queue status in methods, -- as we always know what it should be at any stage of the protocol, -- and in case it does not match use this error. diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 52d3a84ae..9e0e5702c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -29,8 +29,7 @@ where import Control.Concurrent (threadDelay) import Control.Concurrent.STM import Control.Exception (bracket) -import Control.Monad (replicateM_, unless, when) -import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO)) +import Control.Monad.Except import Control.Monad.IO.Unlift (MonadUnliftIO) import Crypto.Random (ChaChaDRG, randomBytesGenerate) import Data.ByteString (ByteString) @@ -40,7 +39,6 @@ import Data.List (find) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeUtf8) import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), SQLData (..), SQLError, field) import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.FromField @@ -54,6 +52,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration) import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Parsers (blobFieldParser) +import Simplex.Messaging.Protocol (MsgBody) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Util (bshow, liftIOEither) import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist) @@ -392,6 +391,51 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto insertSndMsgDetails_ db connId sndMsgData updateHashSnd_ db connId sndMsgData + updateSndMsgStatus :: SQLiteStore -> ConnId -> InternalId -> SndMsgStatus -> m () + updateSndMsgStatus st connId msgId msgStatus = + liftIO . withTransaction st $ \db -> + DB.executeNamed + db + [sql| + UPDATE snd_messages + SET snd_status = :snd_status + WHERE conn_alias = :conn_alias AND internal_id = :internal_id + |] + [ ":conn_alias" := connId, + ":internal_id" := msgId, + ":snd_status" := msgStatus + ] + + getPendingMsgData :: SQLiteStore -> ConnId -> InternalId -> m (SndQueue, MsgBody) + getPendingMsgData st connId msgId = + liftIOEither . withTransaction st $ \db -> runExceptT $ do + sq <- ExceptT $ sndQueue <$> getSndQueueByConnAlias_ db connId + msgBody <- + ExceptT $ + sndMsgData + <$> DB.query + db + [sql| + SELECT m.msg_body + FROM messages m + JOIN snd_messages s ON s.conn_alias = m.conn_alias AND s.internal_id = m.internal_id + WHERE m.conn_alias = ? AND m.internal_id = ? + |] + (connId, msgId) + pure (sq, msgBody) + where + sndMsgData :: [Only MsgBody] -> Either StoreError MsgBody + sndMsgData [Only msgBody] = Right msgBody + sndMsgData _ = Left SEMsgNotFound + sndQueue :: Maybe SndQueue -> Either StoreError SndQueue + sndQueue = maybe (Left SEConnNotFound) Right + + getPendingMsgs :: SQLiteStore -> ConnId -> m [PendingMsg] + getPendingMsgs st connId = + liftIO . withTransaction st $ \db -> + map (PendingMsg connId . fromOnly) + <$> DB.query db "SELECT internal_id FROM snd_messages WHERE conn_alias = ? AND snd_status = ?" (connId, SndMsgCreated) + getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg getMsg _st _connAlias _id = throwError SENotImplemented @@ -676,15 +720,15 @@ insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgBody, internalRcvId} = d dbConn [sql| INSERT INTO messages - ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body) + ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body, msg_body) VALUES - (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL,:body); + (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL, '', :msg_body); |] [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_rcv_id" := internalRcvId, - ":body" := decodeUtf8 msgBody + ":msg_body" := msgBody ] insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () @@ -771,15 +815,15 @@ insertSndMsgBase_ dbConn connId SndMsgData {..} = do dbConn [sql| INSERT INTO messages - ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body) + ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body, msg_body) VALUES - (:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id,:body); + (:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id, '',:msg_body); |] [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_snd_id" := internalSndId, - ":body" := decodeUtf8 msgBody + ":msg_body" := msgBody ] insertSndMsgDetails_ :: DB.Connection -> ConnId -> SndMsgData -> IO () @@ -788,15 +832,16 @@ insertSndMsgDetails_ dbConn connId SndMsgData {..} = dbConn [sql| INSERT INTO snd_messages - ( conn_alias, internal_snd_id, internal_id, snd_status, sent_ts, delivered_ts, internal_hash) + ( conn_alias, internal_snd_id, internal_id, snd_status, sent_ts, delivered_ts, internal_hash, previous_msg_hash) VALUES - (:conn_alias,:internal_snd_id,:internal_id,:snd_status, NULL, NULL,:internal_hash); + (:conn_alias,:internal_snd_id,:internal_id,:snd_status, NULL, NULL,:internal_hash,:previous_msg_hash); |] [ ":conn_alias" := connId, ":internal_snd_id" := internalSndId, ":internal_id" := internalId, ":snd_status" := SndMsgCreated, - ":internal_hash" := internalHash + ":internal_hash" := internalHash, + ":previous_msg_hash" := previousMsgHash ] updateHashSnd_ :: DB.Connection -> ConnId -> SndMsgData -> IO () diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index aa579a175..080db2b55 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -16,10 +16,11 @@ import Control.Concurrent import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient -import SMPClient (withSmpServer) +import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) +import System.Directory (removeFile) import System.Timeout import Test.Hspec @@ -46,6 +47,11 @@ agentTests (ATransport t) = do smpAgentTest3_1_1 $ testSubscription t it "should send notifications to client when server disconnects" $ smpAgentServerTest $ testSubscrNotification t + describe "Message delivery" do + it "should deliver messages after losing server connection and re-connecting" $ + smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t + it "should deliver pending messages after agent restarting" $ + smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t -- | receive message to handle `h` (<#:) :: Transport c => c -> IO (ATransmissionOrError 'Agent) @@ -100,16 +106,21 @@ testDuplexConnection _ alice bob = do bob <# ("", "alice", INFO "alice's connInfo") bob <# ("", "alice", CON) alice <# ("", "bob", CON) - alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", SENT 1) - alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", SENT 2) + alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 1) + alice <# ("", "bob", SENT 1) + alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 2) + alice <# ("", "bob", SENT 2) bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False - bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", SENT 3) + bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 3) + bob <# ("", "alice", SENT 3) alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False - bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", SENT 4) + bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 4) + bob <# ("", "alice", SENT 4) alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) - bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", ERR (SMP AUTH)) + bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 5) + bob <# ("", "alice", MERR 5 (SMP AUTH)) alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" @@ -124,29 +135,37 @@ testDuplexConnRandomIds _ alice bob = do bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) - alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, SENT 1) - alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, SENT 2) + alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 1) + alice <# ("", bobConn, SENT 1) + alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 2) + alice <# ("", bobConn, SENT 2) bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False - bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, SENT 3) + bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 3) + bob <# ("", aliceConn, SENT 3) alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False - bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, SENT 4) + bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 4) + bob <# ("", aliceConn, SENT 4) alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) - bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, ERR (SMP AUTH)) + bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 5) + bob <# ("", aliceConn, MERR 5 (SMP AUTH)) alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) alice #:# "nothing else should be delivered to alice" testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do (alice1, "alice") `connect` (bob, "bob") - bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", SENT 1) - bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", SENT 2) + bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 1) + bob <# ("", "alice", SENT 1) + bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 2) + bob <# ("", "alice", SENT 2) alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK) alice1 <# ("", "bob", END) - bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", SENT 3) + bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 3) + bob <# ("", "alice", SENT 3) alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False alice1 #:# "nothing else should be delivered to alice1" @@ -159,6 +178,61 @@ testSubscrNotification t (server, _) client = do withSmpServer (ATransport t) $ client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue +testMsgDeliveryServerRestart :: Transport c => TProxy c -> c -> c -> IO () +testMsgDeliveryServerRestart t alice bob = do + withServer $ do + connect (alice, "alice") (bob, "bob") + bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 1) + bob <# ("", "alice", SENT 1) + alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False + alice #:# "nothing else delivered before the server is killed" + + alice <# ("", "bob", DOWN) + bob #: ("2", "alice", "SEND 11\nhello again") #> ("2", "alice", MID 2) + bob #:# "nothing else delivered before the server is restarted" + alice #:# "nothing else delivered before the server is restarted" + + withServer $ do + bob <# ("", "alice", SENT 2) + alice <# ("", "bob", UP) + alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False + + removeFile testStoreLogFile + where + withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () + +testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO () +testMsgDeliveryAgentRestart t bob = do + withAgent $ \alice -> do + withServer $ do + connect (bob, "bob") (alice, "alice") + alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 1) + alice <# ("", "bob", SENT 1) + bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False + bob #:# "nothing else delivered before the server is down" + + bob <# ("", "alice", DOWN) + alice #: ("2", "bob", "SEND 11\nhello again") #> ("2", "bob", MID 2) + alice #:# "nothing else delivered before the server is restarted" + bob #:# "nothing else delivered before the server is restarted" + + withAgent $ \alice -> do + withServer $ do + tPutRaw alice ("3", "bob", "SUB") + alice <#= \case + (corrId, "bob", cmd) -> + (corrId == "3" && cmd == OK) + || (corrId == "" && cmd == SENT 2) + _ -> False + bob <# ("", "alice", UP) + bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False + + removeFile testStoreLogFile + removeFile testDB + where + withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` () + withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort + connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") @@ -170,17 +244,17 @@ connect (h1, name1) (h2, name2) = do h2 <# ("", name1, CON) h1 <# ("", name2, CON) -connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) -connect' h1 h2 = do - ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo' <> " 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") - h2 <# ("", conn1, CON) - h1 <# ("", conn2, CON) - pure (conn1, conn2) +-- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) +-- connect' h1 h2 = do +-- ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") +-- let qInfo' = serializeSmpQueueInfo qInfo +-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo' <> " 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") +-- h2 <# ("", conn1, CON) +-- h1 <# ("", conn2, CON) +-- pure (conn1, conn2) samplePublicKey :: ByteString samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR" diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 4d945553d..27b08b44e 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -7,7 +7,7 @@ module AgentTests.FunctionalAPITests (functionalAPITests) where -import Control.Monad.Except (ExceptT, catchError, runExceptT) +import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.IO.Unlift import SMPAgentClient import SMPClient (withSmpServer) @@ -62,15 +62,20 @@ testAgentClient = do get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) InternalId 1 <- sendMessage alice bobId "hello" + get alice ##> ("", bobId, SENT 1) InternalId 2 <- sendMessage alice bobId "how are you?" + get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False InternalId 3 <- sendMessage bob aliceId "hello too" + get bob ##> ("", aliceId, SENT 3) InternalId 4 <- sendMessage bob aliceId "message 1" + get bob ##> ("", aliceId, SENT 4) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False suspendConnection alice bobId - InternalId 0 <- sendMessage bob aliceId "message 2" `catchError` \(SMP AUTH) -> pure $ InternalId 0 + InternalId 5 <- sendMessage bob aliceId "message 2" + get bob ##> ("", aliceId, MERR 5 (SMP AUTH)) deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" pure () @@ -146,6 +151,8 @@ testAsyncBothOffline = do exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetings alice bobId bob aliceId = do InternalId 1 <- sendMessage alice bobId "hello" + get alice ##> ("", bobId, SENT 1) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False InternalId 2 <- sendMessage bob aliceId "hello too" + get bob ##> ("", aliceId, SENT 2) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index f5c64d923..26d652ad9 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -438,7 +438,8 @@ mkSndMsgData internalId internalSndId internalHash = internalSndId, internalTs = ts, msgBody = hw, - internalHash + internalHash, + previousMsgHash = internalHash } testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index af1d81b7e..f967368b1 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -96,11 +96,15 @@ smpAgentTestN_1 n test' = runSmpAgentTestN_1 n test' `shouldReturn` () smpAgentTest2_2_2 :: forall c. Transport c => (c -> c -> IO ()) -> Expectation smpAgentTest2_2_2 test' = withSmpServerOn (transport @c) testPort2 $ - smpAgentTestN - [ (agentTestPort, testPort, testDB), - (agentTestPort2, testPort2, testDB2) - ] - _test + smpAgentTest2_2_2_needs_server test' + +smpAgentTest2_2_2_needs_server :: forall c. Transport c => (c -> c -> IO ()) -> Expectation +smpAgentTest2_2_2_needs_server test' = + smpAgentTestN + [ (agentTestPort, testPort, testDB), + (agentTestPort2, testPort2, testDB2) + ] + _test where _test [h1, h2] = test' h1 h2 _test _ = error "expected 2 handles" @@ -140,6 +144,15 @@ smpAgentTest3_1_1 test' = smpAgentTestN_1 3 _test _test [h1, h2, h3] = test' h1 h2 h3 _test _ = error "expected 3 handles" +smpAgentTest1_1_1 :: forall c. Transport c => (c -> IO ()) -> Expectation +smpAgentTest1_1_1 test' = + smpAgentTestN + [(agentTestPort2, testPort2, testDB2)] + _test + where + _test [h] = test' h + _test _ = error "expected 1 handle" + cfg :: AgentConfig cfg = defaultAgentConfig @@ -156,12 +169,15 @@ cfg = retryInterval = (retryInterval defaultAgentConfig) {initialInterval = 50_000} } -withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a -withSmpAgentThreadOn t (port', smpPort', db') = +withSmpAgentThreadOn_ :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m () -> (ThreadId -> m a) -> m a +withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess = let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" (Just smpPort') testKeyHash]} in serverBracket (\started -> runSMPAgentBlocking t started cfg') - (removeFile db') + afterProcess + +withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a +withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ removeFile db' withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const From cda02a3b30a6074ecc4571f3946f5d85ee470379 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 22 Aug 2021 10:04:49 +0100 Subject: [PATCH 18/29] support 3072 bits RSA key size (#180) --- src/Simplex/Messaging/Crypto.hs | 1 + src/Simplex/Messaging/Server.hs | 4 ++++ tests/ServerTests.hs | 9 ++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index bc709e377..10e7ad5b5 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -256,6 +256,7 @@ validKeySize :: Int -> Bool validKeySize = \case 128 -> True 256 -> True + 384 -> True 512 -> True _ -> False diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 211317cc6..221793d2f 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -157,6 +157,7 @@ verifyTransmission (sig, t@(corrId, queueId, cmd)) = do cryptoVerify $ case sigLen of 128 -> dummyKey128 256 -> dummyKey256 + 384 -> dummyKey384 512 -> dummyKey512 _ -> dummyKey256 sigLen = B.length $ C.unSignature sig @@ -169,6 +170,9 @@ dummyKey128 = "MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQC2oeA7s4roXN5K2N6022I1/ dummyKey256 :: C.PublicKey dummyKey256 = "MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAxwmTvaqmdTbkfUGNi8Yu0L/T4cxuOlQlx3zGZ9X9Qx0+oZjknWK+QHrdWTcpS+zH4Hi7fP6kanOQoQ90Hj6Ghl57VU1GEdUPywSw4i1/7t0Wv9uT9Q2ktHp2rqVo3xkC9IVIpL7EZAxdRviIN2OsOB3g4a/F1ZpjxcAaZeOMUugiAX1+GtkLuE0Xn4neYjCaOghLxQTdhybN70VtnkiQLx/X9NjkDIl/spYGm3tQFMyYKkP6IWoEpj0926hJ0fmlmhy8tAOhlZsb/baW5cgkEZ3E9jVVrySCgQzoLQgma610FIISRpRJbSyv26jU7MkMxiyuBiDaFOORkXFttoKbtQKBgEbDS9II2brsz+vfI7uP8atFcawkE52cx4M1UWQhqb1H3tBiRl+qO+dMq1pPQF2bW7dlZAWYzS4W/367bTAuALHBDGB8xi1P4Njhh9vaOgTvuqrHG9NJQ85BLy0qGw8rjIWSIXVmVpfrXFJ8po5l04UE258Ll2yocv3QRQmddQW9" +dummyKey384 :: C.PublicKey +dummyKey384 = "MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAYEAthExp77lSFBMB0RedjgKIU+oNH5lMGdMqDCG0E5Ly7X49rFpfDMMN08GDIgvzg9kcwV3ScbPcjUE19wmAShX9f9k3w38KM3wmIBKSiuCREQl0V3xAYp1SYwiAkMNSSwxuIkDEeSOR56WdEcZvqbB4lY9MQlUv70KriPDxZaqKCTKslUezXHQuYPQX6eMnGFK7hxz5Kl5MajV52d+5iXsa8CA+m/e1KVnbelCO+xhN89xG8ALt0CJ9k5Wwo3myLgXi4dmNankCmg8jkh+7y2ywkzxMwH1JydDtV/FLzkbZsbPR2w93TNrTq1RJOuqMyh0VtdBSpxNW/Ft988TkkX2BAWzx82INw7W6/QbHGNtHNB995R4sgeYy8QbEpNGBhQnfQh7yRWygLTVXWKApQzzfCeIoDDWUS7dMv/zXoasAnpDBj+6UhHv3BHrps7kBvRyZQ2d/nUuAqiGd43ljJ++n6vNyFLgZoiV7HLia/FOGMkdt7j92CNmFHxiT6Xl7kRHAoGBAPNoWny2O7LBxzAKMLmQVHBAiKp6RMx+7URvtQDHDHPaZ7F3MvtvmYWwGzund3cQFAaV1EkJoYeI3YRuj6xdXgMyMaP54On++btArb6jUtZuvlC98qE8dEEHQNh+7TsCiMU+ivbeKFxS9A/B7OVedoMnPoJWhatbA9zB/6L1GNPh" + dummyKey512 :: C.PublicKey dummyKey512 = "MIICoDANBgkqhkiG9w0BAQEFAAOCAo0AMIICiAKCAgEArkCY9DuverJ4mmzDektv9aZMFyeRV46WZK9NsOBKEc+1ncqMs+LhLti9asKNgUBRbNzmbOe0NYYftrUpwnATaenggkTFxxbJ4JGJuGYbsEdFWkXSvrbWGtM8YUmn5RkAGme12xQ89bSM4VoJAGnrYPHwmcQd+KYCPZvTUsxaxgrJTX65ejHN9BsAn8XtGViOtHTDJO9yUMD2WrJvd7wnNa+0ugEteDLzMU++xS98VC+uA1vfauUqi3yXVchdfrLdVUuM+JE0gUEXCgzjuHkaoHiaGNiGhdPYoAJJdOKQOIHAKdk7Th6OPhirPhc9XYNB4O8JDthKhNtfokvFIFlC4QBRzJhpLIENaEBDt08WmgpOnecZB/CuxkqqOrNa8j5K5jNrtXAI67W46VEC2jeQy/gZwb64Zit2A4D00xXzGbQTPGj4ehcEMhLx5LSCygViEf0w0tN3c3TEyUcgPzvECd2ZVpQLr9Z4a07Ebr+YSuxcHhjg4Rg1VyJyOTTvaCBGm5X2B3+tI4NUttmikIHOYpBnsLmHY2BgfH2KcrIsDyAhInXmTFr/L2+erFarUnlfATd2L8Ti43TNHDedO6k6jI5Gyi62yPwjqPLEIIK8l+pIeNfHJ3pPmjhHBfzFcQLMMMXffHWNK8kWklrQXK+4j4HiPcTBvlO1FEtG9nEIZhUCgYA4a6WtI2k5YNli1C89GY5rGUY7RP71T6RWri/D3Lz9T7GvU+FemAyYmsvCQwqijUOur0uLvwSP8VdxpSUcrjJJSWur2hrPWzWlu0XbNaeizxpFeKbQP+zSrWJ1z8RwfAeUjShxt8q1TuqGqY10wQyp3nyiTGvS+KwZVj5h5qx8NQ==" diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index bca83c073..a3d93093b 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -337,12 +337,19 @@ testTiming (ATransport t) = (testSameTiming rh sh) [ (128, 128, 100), (128, 256, 25), + (128, 384, 15), -- (128, 512, 15), (256, 128, 100), - (256, 256, 25) + (256, 256, 25), + (256, 384, 15), -- (256, 512, 15), + (384, 128, 100), + (384, 256, 25), + (384, 384, 15) + -- (384, 512, 15), -- (512, 128, 100), -- (512, 256, 25), + -- (512, 384, 15), -- (512, 512, 15) ] where From 26b0edabfcc2d3455f6bdd5df7d760f949cc5334 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 22 Aug 2021 13:19:38 +0100 Subject: [PATCH 19/29] refactor with RecordWildCards (#181) --- migrations/20210101_initial.sql | 2 +- src/Simplex/Messaging/Agent.hs | 6 ++++-- src/Simplex/Messaging/Agent/Protocol.hs | 5 ++++- src/Simplex/Messaging/Agent/Store.hs | 2 -- src/Simplex/Messaging/Agent/Store/SQLite.hs | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/migrations/20210101_initial.sql b/migrations/20210101_initial.sql index 050d4b4d1..75716865a 100644 --- a/migrations/20210101_initial.sql +++ b/migrations/20210101_initial.sql @@ -67,7 +67,7 @@ CREATE TABLE IF NOT EXISTS messages( internal_ts TEXT NOT NULL, internal_rcv_id INTEGER, internal_snd_id INTEGER, - body TEXT NOT NULL, + body TEXT NOT NULL, -- deprecated PRIMARY KEY (conn_alias, internal_id), FOREIGN KEY (conn_alias) REFERENCES connections (conn_alias) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index cf875b09f..77fd5f622 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -9,6 +9,7 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -357,7 +358,8 @@ sendMessage' c connId msg = previousMsgHash, agentMessage = A_MSG msg } - msgData = SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = C.sha256Hash msgBody, previousMsgHash} + internalHash = C.sha256Hash msgBody + msgData = SndMsgData {..} createSndMsg st connId msgData pure internalId @@ -555,7 +557,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do let integrity = checkMsgIntegrity prevExtSndId (fst sender) prevRcvMsgHash externalPrevSndHash recipient = (unId internalId, internalTs) msgMeta = MsgMeta {integrity, recipient, sender, broker} - rcvMsg = RcvMsgData {msgMeta, msgBody, internalRcvId, internalHash, externalPrevSndHash} + rcvMsg = RcvMsgData {..} withStore $ \st -> createRcvMsg st connId rcvMsg notify $ MSG msgMeta msgBody diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index e24a91aa3..684a7e828 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -32,6 +32,7 @@ module Simplex.Messaging.Agent.Protocol ACommand (..), AParty (..), SAParty (..), + MsgHash, MsgMeta (..), SMPMessage (..), AMessage (..), @@ -182,6 +183,8 @@ deriving instance Eq (ACommand p) deriving instance Show (ACommand p) +type MsgHash = ByteString + -- | Agent message metadata sent to the client data MsgMeta = MsgMeta { integrity :: MsgIntegrity, @@ -209,7 +212,7 @@ data SMPMessage -- | timestamp from the sending agent senderTimestamp :: SenderTimestamp, -- | digest of the previous message - previousMsgHash :: ByteString, + previousMsgHash :: MsgHash, -- | messages sent between agents once queue is secured agentMessage :: AMessage } diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index c41110c32..bf5e6921f 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -162,8 +162,6 @@ data AcceptedConfirmation = AcceptedConfirmation -- * Message integrity validation types -type MsgHash = ByteString - -- | Corresponds to `last_external_snd_msg_id` in `connections` table type PrevExternalSndId = Int64 diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 9e0e5702c..f74c705b1 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -722,7 +722,7 @@ insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgBody, internalRcvId} = d INSERT INTO messages ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, body, msg_body) VALUES - (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL, '', :msg_body); + (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL, '',:msg_body); |] [ ":conn_alias" := connId, ":internal_id" := internalId, From cb950ae2e4f69f6df4d351e8458925b713ed5757 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 26 Aug 2021 22:54:51 +0100 Subject: [PATCH 20/29] add SMP queue quota to server config (and use TBQueue for messages) (#182) --- apps/smp-server/Main.hs | 1 + src/Simplex/Messaging/Agent.hs | 1 + src/Simplex/Messaging/Protocol.hs | 2 ++ src/Simplex/Messaging/Server.hs | 11 +++++++---- src/Simplex/Messaging/Server/Env/STM.hs | 1 + src/Simplex/Messaging/Server/MsgStore.hs | 4 +++- src/Simplex/Messaging/Server/MsgStore/STM.hs | 20 ++++++++++++-------- tests/SMPClient.hs | 1 + 8 files changed, 28 insertions(+), 13 deletions(-) diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index 05161bde1..0f14d7ca0 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -37,6 +37,7 @@ serverConfig :: ServerConfig serverConfig = ServerConfig { tbqSize = 16, + msgQueueQuota = 256, queueIdBytes = 12, msgIdBytes = 6, -- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 77fd5f622..a3be20c1f 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -412,6 +412,7 @@ runSrvMsgDelivery c@AgentClient {subQ} srv = do withRetryInterval ri $ \loop -> do sendAgentMessage c sq msgBody `catchError` \case + SMP SMP.QUOTA -> loop e@SMP {} -> notify connId $ MERR mId e _ -> loop notify connId $ SENT mId diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 3d438d540..85d6e8369 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -192,6 +192,8 @@ data ErrorType CMD CommandError | -- | command authorization error - bad signature or non-existing SMP queue AUTH + | -- | SMP queue capacity is exceeded on the server + QUOTA | -- | ACK command is sent without message to be acknowledged NO_MSG | -- | internal server error diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 221793d2f..85eb66447 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -296,16 +296,19 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} = QueueActive -> do ms <- asks msgStore msg <- mkMessage + quota <- asks $ msgQueueQuota . config atomically $ do - q <- getMsgQueue ms (recipientId qr) - writeMsg q msg - return ok + q <- getMsgQueue ms (recipientId qr) quota + isFull q >>= \case + False -> writeMsg q msg $> ok + True -> pure $ err QUOTA deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m Transmission deliverMessage tryPeek rId = \case Sub {subThread = NoSub} -> do ms <- asks msgStore - q <- atomically $ getMsgQueue ms rId + quota <- asks $ msgQueueQuota . config + q <- atomically $ getMsgQueue ms rId quota atomically (tryPeek q) >>= \case Nothing -> forkSub q $> ok Just msg -> atomically setDelivered $> mkResp corrId rId (msgCmd msg) diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 83282f03f..5b640db7d 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -25,6 +25,7 @@ import UnliftIO.STM data ServerConfig = ServerConfig { transports :: [(ServiceName, ATransport)], tbqSize :: Natural, + msgQueueQuota :: Natural, queueIdBytes :: Int, msgIdBytes :: Int, storeLog :: Maybe (StoreLog 'ReadMode), diff --git a/src/Simplex/Messaging/Server/MsgStore.hs b/src/Simplex/Messaging/Server/MsgStore.hs index e2cb8791a..3d729af60 100644 --- a/src/Simplex/Messaging/Server/MsgStore.hs +++ b/src/Simplex/Messaging/Server/MsgStore.hs @@ -3,6 +3,7 @@ module Simplex.Messaging.Server.MsgStore where import Data.Time.Clock +import Numeric.Natural import Simplex.Messaging.Protocol (Encoded, MsgBody, RecipientId) data Message = Message @@ -12,10 +13,11 @@ data Message = Message } class MonadMsgStore s q m | s -> q where - getMsgQueue :: s -> RecipientId -> m q + getMsgQueue :: s -> RecipientId -> Natural -> m q delMsgQueue :: s -> RecipientId -> m () class MonadMsgQueue q m where + isFull :: q -> m Bool writeMsg :: q -> Message -> m () -- non blocking tryPeekMsg :: q -> m (Maybe Message) -- non blocking peekMsg :: q -> m Message -- blocking diff --git a/src/Simplex/Messaging/Server/MsgStore/STM.hs b/src/Simplex/Messaging/Server/MsgStore/STM.hs index f5b0e670f..6d0fb63a0 100644 --- a/src/Simplex/Messaging/Server/MsgStore/STM.hs +++ b/src/Simplex/Messaging/Server/MsgStore/STM.hs @@ -8,11 +8,12 @@ module Simplex.Messaging.Server.MsgStore.STM where import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Numeric.Natural import Simplex.Messaging.Protocol (RecipientId) import Simplex.Messaging.Server.MsgStore import UnliftIO.STM -newtype MsgQueue = MsgQueue {msgQueue :: TQueue Message} +newtype MsgQueue = MsgQueue {msgQueue :: TBQueue Message} newtype MsgStoreData = MsgStoreData {messages :: Map RecipientId MsgQueue} @@ -22,13 +23,13 @@ newMsgStore :: STM STMMsgStore newMsgStore = newTVar $ MsgStoreData M.empty instance MonadMsgStore STMMsgStore MsgQueue STM where - getMsgQueue :: STMMsgStore -> RecipientId -> STM MsgQueue - getMsgQueue store rId = do + getMsgQueue :: STMMsgStore -> RecipientId -> Natural -> STM MsgQueue + getMsgQueue store rId quota = do m <- messages <$> readTVar store maybe (newQ m) return $ M.lookup rId m where newQ m' = do - q <- MsgQueue <$> newTQueue + q <- MsgQueue <$> newTBQueue quota writeTVar store . MsgStoreData $ M.insert rId q m' return q @@ -37,15 +38,18 @@ instance MonadMsgStore STMMsgStore MsgQueue STM where modifyTVar store $ MsgStoreData . M.delete rId . messages instance MonadMsgQueue MsgQueue STM where + isFull :: MsgQueue -> STM Bool + isFull = isFullTBQueue . msgQueue + writeMsg :: MsgQueue -> Message -> STM () - writeMsg = writeTQueue . msgQueue + writeMsg = writeTBQueue . msgQueue tryPeekMsg :: MsgQueue -> STM (Maybe Message) - tryPeekMsg = tryPeekTQueue . msgQueue + tryPeekMsg = tryPeekTBQueue . msgQueue peekMsg :: MsgQueue -> STM Message - peekMsg = peekTQueue . msgQueue + peekMsg = peekTBQueue . msgQueue -- atomic delete (== read) last and peek next message if available tryDelPeekMsg :: MsgQueue -> STM (Maybe Message) - tryDelPeekMsg (MsgQueue q) = tryReadTQueue q >> tryPeekTQueue q + tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 5b7dad9c2..5bd38d61e 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -57,6 +57,7 @@ cfg = ServerConfig { transports = undefined, tbqSize = 1, + msgQueueQuota = 4, queueIdBytes = 12, msgIdBytes = 6, storeLog = Nothing, From 7b5ebf7bd2aa639a928bf69908d054b3ac076228 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 26 Aug 2021 23:25:49 +0100 Subject: [PATCH 21/29] change type of agent sendMessage result to AgentMsgId (#183) --- src/Simplex/Messaging/Agent.hs | 10 +++++----- tests/AgentTests/FunctionalAPITests.hs | 15 +++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index a3be20c1f..100f4675f 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -142,7 +142,7 @@ subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () subscribeConnection c = withAgentClient c . subscribeConnection' c -- | Send message to the connection (SEND command) -sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId +sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId sendMessage c = withAgentClient c .: sendMessage' c -- | Suspend SMP agent connection (OFF command) @@ -231,7 +231,7 @@ processCommand c (connId, cmd) = case cmd of JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) - SEND msgBody -> (connId,) . MID . unId <$> sendMessage' c connId msgBody + SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) @@ -327,14 +327,14 @@ subscribeConnection' c connId = pure r {initialInterval = 5_000_000} -- | Send message to the connection (SEND command) in Reader monad -sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m InternalId +sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId sendMessage' c connId msg = withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ _ sq) -> enqueueMessage sq SomeConn _ (SndConnection _ sq) -> enqueueMessage sq _ -> throwError $ CONN SIMPLEX where - enqueueMessage :: SndQueue -> m InternalId + enqueueMessage :: SndQueue -> m AgentMsgId enqueueMessage SndQueue {server} = do msgId <- storeSentMsg wasDelivering <- resumeMsgDelivery c connId server @@ -343,7 +343,7 @@ sendMessage' c connId msg = then pure [PendingMsg {connId, msgId}] else withStore (`getPendingMsgs` connId) queuePendingMsgs c connId pending - pure msgId + pure $ unId msgId where storeSentMsg :: m InternalId storeSentMsg = do diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 27b08b44e..c9903fe85 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -14,7 +14,6 @@ import SMPClient (withSmpServer) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (dbFile) import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store (InternalId (..)) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..)) import System.Timeout @@ -61,20 +60,20 @@ testAgentClient = do get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) - InternalId 1 <- sendMessage alice bobId "hello" + 1 <- sendMessage alice bobId "hello" get alice ##> ("", bobId, SENT 1) - InternalId 2 <- sendMessage alice bobId "how are you?" + 2 <- sendMessage alice bobId "how are you?" get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False - InternalId 3 <- sendMessage bob aliceId "hello too" + 3 <- sendMessage bob aliceId "hello too" get bob ##> ("", aliceId, SENT 3) - InternalId 4 <- sendMessage bob aliceId "message 1" + 4 <- sendMessage bob aliceId "message 1" get bob ##> ("", aliceId, SENT 4) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False suspendConnection alice bobId - InternalId 5 <- sendMessage bob aliceId "message 2" + 5 <- sendMessage bob aliceId "message 2" get bob ##> ("", aliceId, MERR 5 (SMP AUTH)) deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" @@ -150,9 +149,9 @@ testAsyncBothOffline = do exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetings alice bobId bob aliceId = do - InternalId 1 <- sendMessage alice bobId "hello" + 1 <- sendMessage alice bobId "hello" get alice ##> ("", bobId, SENT 1) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False - InternalId 2 <- sendMessage bob aliceId "hello too" + 2 <- sendMessage bob aliceId "hello too" get bob ##> ("", aliceId, SENT 2) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False From 96c633930666ab52db97fe600e23b98a515161a6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 29 Aug 2021 13:10:59 +0100 Subject: [PATCH 22/29] fix error handling of sending pending messages; set block size to 8192 (#184) * fix error handling of sending pending messages * s/tryAction/tryError/ --- src/Simplex/Messaging/Agent.hs | 21 +++++++++++---------- src/Simplex/Messaging/Transport.hs | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 100f4675f..76145ec0a 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -403,21 +403,22 @@ runSrvMsgDelivery c@AgentClient {subQ} srv = do forever $ do PendingMsg {connId, msgId} <- atomically $ readTQueue mq let mId = unId msgId - r <- withStore $ \st -> - (Right <$> getPendingMsgData st connId msgId) - `E.catch` \(e :: E.SomeException) -> pure $ Left e - case r of - Left e -> notify connId $ MERR mId (INTERNAL $ show e) + withStore (\st -> E.try $ getPendingMsgData st connId msgId) >>= \case + Left (e :: E.SomeException) -> + notify connId $ MERR mId (INTERNAL $ show e) Right (sq, msgBody) -> do withRetryInterval ri $ \loop -> do - sendAgentMessage c sq msgBody - `catchError` \case + tryError (sendAgentMessage c sq msgBody) >>= \case + Left e -> case e of SMP SMP.QUOTA -> loop - e@SMP {} -> notify connId $ MERR mId e + SMP {} -> notify connId $ MERR mId e + CMD {} -> notify connId $ MERR mId e _ -> loop - notify connId $ SENT mId - withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent + Right () -> do + notify connId $ SENT mId + withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent where + tryError action = (Right <$> action) `catchError` (pure . Left) notify :: ConnId -> ACommand 'Agent -> m () notify connId cmd = atomically $ writeTBQueue subQ ("", connId, cmd) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 18e05260b..590995a4f 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -427,7 +427,7 @@ binaryRsaTransport :: Int binaryRsaTransport = 0 transportBlockSize :: Int -transportBlockSize = 4096 +transportBlockSize = 8192 maxTransportBlockSize :: Int maxTransportBlockSize = 65536 From 75259d70cfe8b6cc1b872958b70a54770c737c37 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 30 Aug 2021 10:47:22 +0100 Subject: [PATCH 23/29] set block size to 4096 bytes (#185) --- src/Simplex/Messaging/Transport.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 590995a4f..18e05260b 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -427,7 +427,7 @@ binaryRsaTransport :: Int binaryRsaTransport = 0 transportBlockSize :: Int -transportBlockSize = 8192 +transportBlockSize = 4096 maxTransportBlockSize :: Int maxTransportBlockSize = 65536 From 68f24f90d11997f0ecc0cbe55df4728a35857845 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:29:13 +0100 Subject: [PATCH 24/29] export withAgentLock and do not acquire agent lock in functional API (#186) --- src/Simplex/Messaging/Agent.hs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 76145ec0a..d43473f65 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -40,6 +40,7 @@ module Simplex.Messaging.Agent AgentErrorMonad, getSMPAgentClient, disconnectAgentClient, -- used in tests + withAgentLock, createConnection, joinConnection, acceptConnection, @@ -127,34 +128,37 @@ type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) -- | Create SMP agent connection (NEW command) createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo) -createConnection c = withAgentClient c $ newConn c "" +createConnection c = withAgentEnv c $ newConn c "" -- | Join SMP agent connection (JOIN command) joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConnection c = withAgentClient c .: joinConn c "" +joinConnection c = withAgentEnv c .: joinConn c "" -- | Approve confirmation (LET command) acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -acceptConnection c = withAgentClient c .:. acceptConnection' c +acceptConnection c = withAgentEnv c .:. acceptConnection' c -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -subscribeConnection c = withAgentClient c . subscribeConnection' c +subscribeConnection c = withAgentEnv c . subscribeConnection' c -- | Send message to the connection (SEND command) sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId -sendMessage c = withAgentClient c .: sendMessage' c +sendMessage c = withAgentEnv c .: sendMessage' c -- | Suspend SMP agent connection (OFF command) suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -suspendConnection c = withAgentClient c . suspendConnection' c +suspendConnection c = withAgentEnv c . suspendConnection' c -- | Delete SMP agent connection (DEL command) deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () -deleteConnection c = withAgentClient c . deleteConnection' c +deleteConnection c = withAgentEnv c . deleteConnection' c -withAgentClient :: AgentErrorMonad m => AgentClient -> ReaderT Env m a -> m a -withAgentClient c = withAgentLock c . (`runReaderT` agentEnv c) +withAgentEnv :: AgentClient -> ReaderT Env m a -> m a +withAgentEnv c = (`runReaderT` agentEnv c) + +-- withAgentClient :: AgentErrorMonad m => AgentClient -> ReaderT Env m a -> m a +-- withAgentClient c = withAgentLock c . withAgentEnv c -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient From 9a3afda29041cb3946b86a8ac04698f43126c954 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 1 Sep 2021 08:29:12 +0100 Subject: [PATCH 25/29] ACK command, ackMessage, do not send ACK to SMP server on MSG (after the initial handshake) (#187) --- src/Simplex/Messaging/Agent.hs | 27 +++++++++++++++---- src/Simplex/Messaging/Agent/Protocol.hs | 5 +++- src/Simplex/Messaging/Agent/Store.hs | 2 ++ src/Simplex/Messaging/Agent/Store/SQLite.hs | 30 ++++++++++++++++++++- tests/AgentTests.hs | 15 +++++++++++ tests/AgentTests/FunctionalAPITests.hs | 6 +++++ 6 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index d43473f65..1dd0b4c77 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -46,6 +46,7 @@ module Simplex.Messaging.Agent acceptConnection, subscribeConnection, sendMessage, + ackMessage, suspendConnection, deleteConnection, ) @@ -146,6 +147,9 @@ subscribeConnection c = withAgentEnv c . subscribeConnection' c sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId sendMessage c = withAgentEnv c .: sendMessage' c +ackMessage :: AgentErrorMonad m => AgentClient -> ConnId -> AgentMsgId -> m () +ackMessage c = withAgentEnv c .: ackMessage' c + -- | Suspend SMP agent connection (OFF command) suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () suspendConnection c = withAgentEnv c . suspendConnection' c @@ -236,6 +240,7 @@ processCommand c (connId, cmd) = case cmd of ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody + ACK msgId -> ackMessage' c connId msgId $> (connId, OK) OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) @@ -426,6 +431,20 @@ runSrvMsgDelivery c@AgentClient {subQ} srv = do notify :: ConnId -> ACommand 'Agent -> m () notify connId cmd = atomically $ writeTBQueue subQ ("", connId, cmd) +ackMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> AgentMsgId -> m () +ackMessage' c connId msgId = do + withStore (`getConn` connId) >>= \case + SomeConn _ (DuplexConnection _ rq _) -> ack rq + SomeConn _ (RcvConnection _ rq) -> ack rq + _ -> throwError $ CONN SIMPLEX + where + ack :: RcvQueue -> m () + ack rq = do + let mId = InternalId msgId + withStore $ \st -> checkRcvMsg st connId mId + sendAck c rq + withStore $ \st -> updateRcvMsgAck st connId mId + -- | Suspend SMP agent connection (OFF command) in Reader monad suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m () suspendConnection' c connId = @@ -491,14 +510,12 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do let msgHash = C.sha256Hash msg case parseSMPMessage msg of Left e -> notify $ ERR e - Right (SMPConfirmation senderKey cInfo) -> smpConfirmation senderKey cInfo + Right (SMPConfirmation senderKey cInfo) -> smpConfirmation senderKey cInfo >> sendAck c rq Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> case agentMessage of - HELLO verifyKey _ -> helloMsg verifyKey msgBody - REPLY qInfo -> replyMsg qInfo + HELLO verifyKey _ -> helloMsg verifyKey msgBody >> sendAck c rq + REPLY qInfo -> replyMsg qInfo >> sendAck c rq A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash - sendAck c rq - return () SMP.END -> do removeSubscription c connId logServer "<--" c srv rId "END" diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 684a7e828..c704960a2 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -172,7 +172,7 @@ data ACommand (p :: AParty) where SENT :: AgentMsgId -> ACommand Agent MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent MSG :: MsgMeta -> MsgBody -> ACommand Agent - -- ACK :: AgentMsgId -> ACommand Client + ACK :: AgentMsgId -> ACommand Client -- RCVD :: AgentMsgId -> ACommand Agent OFF :: ACommand Client DEL :: ACommand Client @@ -469,6 +469,7 @@ commandP = <|> "SENT " *> sentResp <|> "MERR " *> msgErrResp <|> "MSG " *> message + <|> "ACK " *> ackCmd <|> "OFF" $> ACmd SClient OFF <|> "DEL" $> ACmd SClient DEL <|> "ERR " *> agentError @@ -485,6 +486,7 @@ commandP = sentResp = ACmd SAgent . SENT <$> A.decimal msgErrResp = ACmd SAgent <$> (MERR <$> A.decimal <* A.space <*> agentErrorTypeP) message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString) + ackCmd = ACmd SClient . ACK <$> A.decimal msgMetaP = do integrity <- msgIntegrityP recipient <- " R=" *> partyMeta A.decimal @@ -526,6 +528,7 @@ serializeCommand = \case MERR mId e -> "MERR " <> bshow mId <> " " <> serializeAgentError e MSG msgMeta msgBody -> "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeBinary msgBody + ACK mId -> "ACK " <> bshow mId OFF -> "OFF" DEL -> "DEL" CON -> "CON" diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index bf5e6921f..fd8b3ced6 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -60,6 +60,8 @@ class Monad m => MonadAgentStore s m where getPendingMsgData :: s -> ConnId -> InternalId -> m (SndQueue, MsgBody) getPendingMsgs :: s -> ConnId -> m [PendingMsg] getMsg :: s -> ConnId -> InternalId -> m Msg + checkRcvMsg :: s -> ConnId -> InternalId -> m () + updateRcvMsgAck :: s -> ConnId -> InternalId -> m () -- * Queue types diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index f74c705b1..5d63af27c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -437,7 +437,35 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto <$> DB.query db "SELECT internal_id FROM snd_messages WHERE conn_alias = ? AND snd_status = ?" (connId, SndMsgCreated) getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg - getMsg _st _connAlias _id = throwError SENotImplemented + getMsg _st _connId _id = throwError SENotImplemented + + checkRcvMsg :: SQLiteStore -> ConnId -> InternalId -> m () + checkRcvMsg st connId msgId = + liftIOEither . withTransaction st $ \db -> + hasMsg + <$> DB.query + db + [sql| + SELECT conn_alias, internal_id + FROM rcv_messages + WHERE conn_alias = ? AND internal_id = ? + |] + (connId, msgId) + where + hasMsg :: [(ConnId, InternalId)] -> Either StoreError () + hasMsg r = if null r then Left SEMsgNotFound else Right () + + updateRcvMsgAck :: SQLiteStore -> ConnId -> InternalId -> m () + updateRcvMsgAck st connId msgId = + liftIO . withTransaction st $ \db -> do + DB.execute + db + [sql| + UPDATE rcv_messages + SET rcv_status = ?, ack_brocker_ts = datetime('now') + WHERE conn_alias = ? AND internal_id = ? + |] + (AcknowledgedToBroker, connId, msgId) -- * Auxiliary helpers diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 080db2b55..545bcece4 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -111,13 +111,17 @@ testDuplexConnection _ alice bob = do alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 2) alice <# ("", "bob", SENT 2) bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False + bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK) bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False + bob #: ("13", "alice", "ACK 2") #> ("13", "alice", OK) bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 3) bob <# ("", "alice", SENT 3) alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False + alice #: ("3a", "bob", "ACK 3") #> ("3a", "bob", OK) bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 4) bob <# ("", "alice", SENT 4) alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False + alice #: ("4a", "bob", "ACK 4") #> ("4a", "bob", OK) alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 5) bob <# ("", "alice", MERR 5 (SMP AUTH)) @@ -140,13 +144,17 @@ testDuplexConnRandomIds _ alice bob = do alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 2) alice <# ("", bobConn, SENT 2) bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False + bob #: ("12", aliceConn, "ACK 1") #> ("12", aliceConn, OK) bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False + bob #: ("13", aliceConn, "ACK 2") #> ("13", aliceConn, OK) bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 3) bob <# ("", aliceConn, SENT 3) alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False + alice #: ("3a", bobConn, "ACK 3") #> ("3a", bobConn, OK) bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 4) bob <# ("", aliceConn, SENT 4) alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False + alice #: ("4a", bobConn, "ACK 4") #> ("4a", bobConn, OK) alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 5) bob <# ("", aliceConn, MERR 5 (SMP AUTH)) @@ -161,12 +169,15 @@ testSubscription _ alice1 alice2 bob = do bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 2) bob <# ("", "alice", SENT 2) alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False + alice1 #: ("1", "bob", "ACK 1") #> ("1", "bob", OK) alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False + alice1 #: ("2", "bob", "ACK 2") #> ("2", "bob", OK) alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK) alice1 <# ("", "bob", END) bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 3) bob <# ("", "alice", SENT 3) alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False + alice2 #: ("22", "bob", "ACK 3") #> ("22", "bob", OK) alice1 #:# "nothing else should be delivered to alice1" testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () @@ -185,6 +196,7 @@ testMsgDeliveryServerRestart t alice bob = do bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 1) bob <# ("", "alice", SENT 1) alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False + alice #: ("11", "bob", "ACK 1") #> ("11", "bob", OK) alice #:# "nothing else delivered before the server is killed" alice <# ("", "bob", DOWN) @@ -196,6 +208,7 @@ testMsgDeliveryServerRestart t alice bob = do bob <# ("", "alice", SENT 2) alice <# ("", "bob", UP) alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False + alice #: ("12", "bob", "ACK 2") #> ("12", "bob", OK) removeFile testStoreLogFile where @@ -209,6 +222,7 @@ testMsgDeliveryAgentRestart t bob = do alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 1) alice <# ("", "bob", SENT 1) bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False + bob #: ("11", "alice", "ACK 1") #> ("11", "alice", OK) bob #:# "nothing else delivered before the server is down" bob <# ("", "alice", DOWN) @@ -226,6 +240,7 @@ testMsgDeliveryAgentRestart t bob = do _ -> False bob <# ("", "alice", UP) bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False + bob #: ("12", "alice", "ACK 2") #> ("12", "alice", OK) removeFile testStoreLogFile removeFile testDB diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index c9903fe85..008e0c14b 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -65,13 +65,17 @@ testAgentClient = do 2 <- sendMessage alice bobId "how are you?" get alice ##> ("", bobId, SENT 2) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + ackMessage bob aliceId 1 get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False + ackMessage bob aliceId 2 3 <- sendMessage bob aliceId "hello too" get bob ##> ("", aliceId, SENT 3) 4 <- sendMessage bob aliceId "message 1" get bob ##> ("", aliceId, SENT 4) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + ackMessage alice bobId 3 get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False + ackMessage alice bobId 4 suspendConnection alice bobId 5 <- sendMessage bob aliceId "message 2" get bob ##> ("", aliceId, MERR 5 (SMP AUTH)) @@ -152,6 +156,8 @@ exchangeGreetings alice bobId bob aliceId = do 1 <- sendMessage alice bobId "hello" get alice ##> ("", bobId, SENT 1) get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + ackMessage bob aliceId 1 2 <- sendMessage bob aliceId "hello too" get bob ##> ("", aliceId, SENT 2) get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + ackMessage alice bobId 2 From d23254f44a8ce066b74928cb57d4690adc69c600 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 3 Sep 2021 20:16:10 +0100 Subject: [PATCH 26/29] SMP block size via config, the client can override size set by the server (#188) --- apps/smp-server/Main.hs | 17 +++++++-- src/Simplex/Messaging/Client.hs | 8 +++-- src/Simplex/Messaging/Server.hs | 3 +- src/Simplex/Messaging/Server/Env/STM.hs | 1 + src/Simplex/Messaging/Transport.hs | 46 +++++++++++++------------ tests/SMPClient.hs | 6 +++- 6 files changed, 52 insertions(+), 29 deletions(-) diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index 0f14d7ca0..b7e87f703 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -29,10 +29,14 @@ import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile) import System.Exit (exitFailure) import System.FilePath (combine) import System.IO (IOMode (..), hFlush, stdout) +import Text.Read (readEither) defaultServerPort :: ServiceName defaultServerPort = "5223" +defaultBlockSize :: Int +defaultBlockSize = 4096 + serverConfig :: ServerConfig serverConfig = ServerConfig @@ -43,6 +47,7 @@ serverConfig = -- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini transports = undefined, storeLog = undefined, + blockSize = undefined, serverPrivateKey = undefined } @@ -97,9 +102,9 @@ getConfig opts = do pure $ makeConfig ini pk storeLog makeConfig :: IniOpts -> C.FullPrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig -makeConfig IniOpts {serverPort, enableWebsockets} pk storeLog = +makeConfig IniOpts {serverPort, blockSize, enableWebsockets} pk storeLog = let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets] - in serverConfig {serverPrivateKey = pk, storeLog, transports} + in serverConfig {serverPrivateKey = pk, storeLog, blockSize, transports} printConfig :: ServerConfig -> IO () printConfig ServerConfig {serverPrivateKey, storeLog} = do @@ -139,6 +144,7 @@ data IniOpts = IniOpts storeLogFile :: FilePath, serverKeyFile :: FilePath, serverPort :: ServiceName, + blockSize :: Int, enableWebsockets :: Bool } @@ -151,7 +157,8 @@ readIni = do serverKeyFile = opt defaultKeyFile "TRANSPORT" "key_file" ini serverPort = opt defaultServerPort "TRANSPORT" "port" ini enableWebsockets = (== Right "on") $ lookupValue "TRANSPORT" "websockets" ini - pure IniOpts {enableStoreLog, storeLogFile, serverKeyFile, serverPort, enableWebsockets} + blockSize <- liftEither . readEither $ opt (show defaultBlockSize) "TRANSPORT" "block_size" ini + pure IniOpts {enableStoreLog, storeLogFile, serverKeyFile, serverPort, blockSize, enableWebsockets} where opt :: String -> Text -> Text -> Ini -> String opt def section key ini = either (const def) T.unpack $ lookupValue section key ini @@ -177,6 +184,9 @@ createIni ServerOpts {enableStoreLog} = do <> "\n\ \# port: " <> defaultServerPort + <> "\n\ + \# block_size: " + <> show defaultBlockSize <> "\n\ \websockets: on\n" pure @@ -185,6 +195,7 @@ createIni ServerOpts {enableStoreLog} = do storeLogFile = defaultStoreLogFile, serverKeyFile = defaultKeyFile, serverPort = defaultServerPort, + blockSize = defaultBlockSize, enableWebsockets = True } diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 960f86ba4..87b340aae 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -99,6 +99,9 @@ data SMPClientConfig = SMPClientConfig tcpTimeout :: Int, -- | period for SMP ping commands (microseconds) smpPing :: Int, + -- | SMP transport block size, Nothing - the block size will be set by the server. + -- Allowed sizes are 4, 8, 16, 32, 64 KiB (* 1024 bytes). + smpBlockSize :: Maybe Int, -- | estimated maximum size of SMP command excluding message body, -- determines the maximum allowed message size smpCommandSize :: Int @@ -112,6 +115,7 @@ smpDefaultConfig = defaultTransport = ("5223", transport @TCP), tcpTimeout = 4_000_000, smpPing = 30_000_000, + smpBlockSize = Just 8192, smpCommandSize = 256 } @@ -128,7 +132,7 @@ type Response = Either SMPClientError Cmd -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. getSMPClient :: SMPServer -> SMPClientConfig -> TBQueue SMPServerTransmission -> IO () -> IO (Either SMPClientError SMPClient) -getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ disconnected = +getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing, smpBlockSize} msgQ disconnected = atomically mkSMPClient >>= runClient useTransport where mkSMPClient :: STM SMPClient @@ -173,7 +177,7 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ dis client :: forall c. Transport c => TProxy c -> SMPClient -> TMVar (Either SMPClientError Int) -> c -> IO () client _ c thVar h = - runExceptT (clientHandshake h $ keyHash smpServer) >>= \case + runExceptT (clientHandshake h smpBlockSize $ keyHash smpServer) >>= \case Left e -> atomically . putTMVar thVar . Left $ SMPTransportError e Right th -> do atomically $ do diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 85eb66447..8c475f9d1 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -90,7 +90,8 @@ runSMPServerBlocking started cfg@ServerConfig {transports} = do runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m () runClient _ h = do keyPair <- asks serverKeyPair - liftIO (runExceptT $ serverHandshake h keyPair) >>= \case + ServerConfig {blockSize} <- asks config + liftIO (runExceptT $ serverHandshake h blockSize keyPair) >>= \case Right th -> runClientTransport th Left _ -> pure () diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 5b640db7d..5c397096b 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -29,6 +29,7 @@ data ServerConfig = ServerConfig queueIdBytes :: Int, msgIdBytes :: Int, storeLog :: Maybe (StoreLog 'ReadMode), + blockSize :: Int, serverPrivateKey :: C.FullPrivateKey -- serverId :: ByteString } diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 18e05260b..d8869f436 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -63,6 +63,7 @@ import Data.ByteArray (xor) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) +import Data.Maybe(fromMaybe) import Data.Set (Set) import qualified Data.Set as S import Data.Word (Word32) @@ -340,21 +341,21 @@ makeNextIV SessionKey {baseIV, counter} = atomically $ do -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -- -- The numbers in function names refer to the steps in the document. -serverHandshake :: forall c. Transport c => c -> C.FullKeyPair -> ExceptT TransportError IO (THandle c) -serverHandshake c (k, pk) = do +serverHandshake :: forall c. Transport c => c -> Int -> C.FullKeyPair -> ExceptT TransportError IO (THandle c) +serverHandshake c srvBlockSize (k, pk) = do + checkValidBlockSize srvBlockSize liftIO sendHeaderAndPublicKey_1 encryptedKeys <- receiveEncryptedKeys_4 - -- TODO server currently ignores blockSize returned by the client - -- this is reserved for future support of streams - ClientHandshake {blockSize = _, sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys - th <- liftIO $ transportHandle c rcvKey sndKey transportBlockSize -- keys are swapped here + ClientHandshake {blockSize, sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys + checkValidBlockSize blockSize + th <- liftIO $ transportHandle c rcvKey sndKey blockSize -- keys are swapped here sendWelcome_6 th pure th where sendHeaderAndPublicKey_1 :: IO () sendHeaderAndPublicKey_1 = do let sKey = C.encodePubKey k - header = ServerHeader {blockSize = transportBlockSize, keySize = B.length sKey} + header = ServerHeader {blockSize = srvBlockSize, keySize = B.length sKey} cPut c $ binaryServerHeader header cPut c sKey receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString @@ -374,13 +375,14 @@ serverHandshake c (k, pk) = do -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -- -- The numbers in function names refer to the steps in the document. -clientHandshake :: forall c. Transport c => c -> Maybe C.KeyHash -> ExceptT TransportError IO (THandle c) -clientHandshake c keyHash = do +clientHandshake :: forall c. Transport c => c -> Maybe Int -> Maybe C.KeyHash -> ExceptT TransportError IO (THandle c) +clientHandshake c blkSize_ keyHash = do + mapM_ checkValidBlockSize blkSize_ (k, blkSize) <- getHeaderAndPublicKey_1_2 - -- TODO currently client always uses the blkSize returned by the server - keys@ClientHandshake {sndKey, rcvKey} <- liftIO $ generateKeys_3 blkSize - sendEncryptedKeys_4 k keys - th <- liftIO $ transportHandle c sndKey rcvKey blkSize + let clientBlkSize = fromMaybe blkSize blkSize_ + chs@ClientHandshake {sndKey, rcvKey} <- liftIO $ generateKeys_3 clientBlkSize + sendEncryptedKeys_4 k chs + th <- liftIO $ transportHandle c sndKey rcvKey clientBlkSize getWelcome_6 th >>= checkVersion pure th where @@ -388,8 +390,7 @@ clientHandshake c keyHash = do getHeaderAndPublicKey_1_2 = do header <- liftIO (cGet c serverHeaderSize) ServerHeader {blockSize, keySize} <- liftEither $ parse serverHeaderP (TEHandshake HEADER) header - when (blockSize < transportBlockSize || blockSize > maxTransportBlockSize) $ - throwError $ TEHandshake HEADER + checkValidBlockSize blockSize s <- liftIO $ cGet c keySize maybe (pure ()) (validateKeyHash_2 s) keyHash key <- liftEither $ parseKey s @@ -408,8 +409,8 @@ clientHandshake c keyHash = do baseIV <- C.randomIV pure SessionKey {aesKey, baseIV, counter = undefined} sendEncryptedKeys_4 :: C.PublicKey -> ClientHandshake -> ExceptT TransportError IO () - sendEncryptedKeys_4 k keys = - liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake keys) + sendEncryptedKeys_4 k chs = + liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake chs) >>= liftIO . cPut c getWelcome_6 :: THandle c -> ExceptT TransportError IO SMPVersion getWelcome_6 th = ExceptT $ (>>= parseSMPVersion) <$> tGetEncrypted th @@ -420,17 +421,18 @@ clientHandshake c keyHash = do when (major smpVersion > major currentSMPVersion) . throwE $ TEHandshake MAJOR_VERSION +checkValidBlockSize :: Int -> ExceptT TransportError IO () +checkValidBlockSize blkSize = + when (blkSize `notElem` transportBlockSizes) . throwError $ TEHandshake HEADER + data ServerHeader = ServerHeader {blockSize :: Int, keySize :: Int} deriving (Eq, Show) binaryRsaTransport :: Int binaryRsaTransport = 0 -transportBlockSize :: Int -transportBlockSize = 4096 - -maxTransportBlockSize :: Int -maxTransportBlockSize = 65536 +transportBlockSizes :: [Int] +transportBlockSizes = map (* 1024) [4, 8, 16, 32, 64] serverHeaderSize :: Int serverHeaderSize = 8 diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 5bd38d61e..58a5d5163 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -39,6 +39,9 @@ testPort2 = "5001" testKeyHashStr :: B.ByteString testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" +testBlockSize :: Maybe Int +testBlockSize = Just 8192 + testKeyHash :: Maybe C.KeyHash testKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" @@ -48,7 +51,7 @@ testStoreLogFile = "tests/tmp/smp-server-store.log" testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a testSMPClient client = runTransportClient testHost testPort $ \h -> - liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case + liftIO (runExceptT $ clientHandshake h testBlockSize testKeyHash) >>= \case Right th -> client th Left e -> error $ show e @@ -61,6 +64,7 @@ cfg = queueIdBytes = 12, msgIdBytes = 6, storeLog = Nothing, + blockSize = 8192, serverPrivateKey = -- full RSA private key (only for tests) "MIIFIwIBAAKCAQEArZyrri/NAwt5buvYjwu+B/MQeJUszDBpRgVqNddlI9kNwDXu\ From 2ac903a2dd37c11a8612b19cd132cf43fe771fe4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 3 Sep 2021 20:35:48 +0100 Subject: [PATCH 27/29] do not allow "," and ";" separators in smp-server hostname (#189) --- src/Simplex/Messaging/Agent/Protocol.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index c704960a2..518b4219d 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -283,7 +283,7 @@ smpQueueInfoP = smpServerP :: Parser SMPServer smpServerP = SMPServer <$> server <*> optional port <*> optional kHash where - server = B.unpack <$> A.takeWhile1 (A.notInClass ":# ") + server = B.unpack <$> A.takeWhile1 (A.notInClass ":#,; ") port = A.char ':' *> (B.unpack <$> A.takeWhile1 A.isDigit) kHash = C.KeyHash <$> (A.char '#' *> base64P) From e07bedac0e59346f076d71635ce6cd25de67ca7e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 4 Sep 2021 19:01:54 +0100 Subject: [PATCH 28/29] move tryError to Simplex.Messaging.Util (#191) --- src/Simplex/Messaging/Agent.hs | 3 +-- src/Simplex/Messaging/Util.hs | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 1dd0b4c77..6b003250c 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -83,7 +83,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (MsgBody, SenderPublicKey) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) -import Simplex.Messaging.Util (bshow) +import Simplex.Messaging.Util (bshow, tryError) import System.Random (randomR) import UnliftIO.Async (Async, async, race_) import qualified UnliftIO.Exception as E @@ -427,7 +427,6 @@ runSrvMsgDelivery c@AgentClient {subQ} srv = do notify connId $ SENT mId withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent where - tryError action = (Right <$> action) `catchError` (pure . Left) notify :: ConnId -> ACommand 'Agent -> m () notify connId cmd = atomically $ writeTBQueue subQ ("", connId, cmd) diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index 2800e521e..5bd05c4a9 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -50,3 +50,6 @@ liftError f = liftEitherError f . runExceptT liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a liftEitherError f a = liftIOEither (first f <$> a) + +tryError :: MonadError e m => m a -> m (Either e a) +tryError action = (Right <$> action) `catchError` (pure . Left) From d973c834a56b7447a1d22aa06fdff2fa827db1e6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 7 Sep 2021 15:25:04 +0100 Subject: [PATCH 29/29] update protocols (#190) Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- protocol/agent-protocol.md | 90 ++++++++++++++----- .../duplex-messaging/duplex-creating.mmd | 16 +++- .../duplex-messaging/duplex-creating.svg | 2 +- protocol/simplex-messaging.md | 5 +- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 94ee8d914..9d80beef0 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -16,9 +16,10 @@ - [Client commands and server responses](#client-commands-and-server-responses) - [NEW command and INV response](#new-command-and-inv-response) - [JOIN command](#join-command) - - [CON notification](#con-notification) + - [REQ notification and ACPT command](#req-notification-and-acpt-command) + - [INFO and CON notifications](#info-and-con-notifications) - [SUB command](#sub-command) - - [SEND command and SENT response](#send-command-and-sent-response) + - [SEND command and MID, SENT and MERR responses](#send-command-and-mid-sent-and-merr-responses) - [MSG notification](#msg-notification) - [END notification](#end-notification) - [OFF command](#off-command) @@ -73,18 +74,22 @@ SMP agent protocol has 3 main parts: The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection comprised of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: 1. Alice requests the new connection from the SMP agent A using `NEW` command. -2. Agent A creates an SMP queue on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection invitation](#connection-invitation). +2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection invitation](#connection-invitation). 3. Alice sends the invitation to Bob via any secure channel they have (out-of-band message). 4. Bob sends `JOIN` command with the invitation as a parameter to agent B to accept the connection. -5. Establishing Alice's SMP queue (with SMP protocol commands): - - Agent B sends unauthenticated message to SMP queue with ephemeral key that will be used to authenticate commands to the queue, as described in SMP protocol. - - Agent A receives the KEY and secures the queue. +5. Establishing Alice's SMP connection (with SMP protocol commands): + - Agent B sends an "SMP confirmation" to the SMP queue specified in the invitation - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info. + - Agent A receives the SMP confirmation containing Bob's key and info. + - Agent A notifies Alice sending REQ notification with Bob's info. + - Alice accepts connection request with ACPT command. + - Agent A secures the queue. - Agent B tries sending authenticated SMP SEND command with agent `HELLO` message until it succeeds. Once it succeeds, Bob's agent "knows" the queue is secured. 6. Agent B creates a new SMP queue on the server. 7. Establish Bob's SMP queue: - Agent B sends `REPLY` message with the invitation to this 2nd queue to Alice's agent (via the 1st queue). - - Agent A having received this `REPLY` message sends unauthenticated message to SMP queue with Alice agent's ephemeral key that will be used to authenticate commands to the queue, as described in SMP protocol. - - Bob's agent receives the key and secures the queue. + - Agent A, having received this `REPLY` message, sends unauthenticated message to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info. + - Bob's agent receives the key and Alice's information and secures the queue. + - Bob's agent sends the notification `INFO` with Alice's information to Bob. - Alice's agent keeps sending `HELLO` message until it succeeds. 8. Agents A and B notify Alice and Bob that connection is established. - Once sending `HELLO` succeeds, Alice's agent sends to Alice `CON` notification that confirms that now both parties can communicate. @@ -193,13 +198,25 @@ cId = encoded cName = 1*(ALPHA / DIGIT / "_" / "-") agentCommand = (userCmd / agentMsg) CRLF -userCmd = newCmd / joinCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd -agentMsg = invitation / connected / unsubscribed / message / sent / received / ok / error +userCmd = newCmd / joinCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd +agentMsg = invitation / connRequest / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error newCmd = %s"NEW" [SP %s"NO_ACK"] ; response is `invitation` or `error` +; NO_ACK parameter currently not supported invitation = %s"INV" SP ; `queueInfo` is the same as in out-of-band message, see SMP protocol +connRequest = %s"REQ" SP confirmationId SP msgBody +; msgBody here is any binary information identifying connection request + +confirmationId = 1*DIGIT + +acceptCmd = %s"ACPT" SP confirmationId SP msgBody +; msgBody here is any binary information identifying connecting party + +connInfo = %s"INFO" SP msgBody +; msgBody here is any binary information identifying connecting party + connected = %s"CON" subscribeCmd = %s"SUB" ; response is `ok` or `error` @@ -208,6 +225,12 @@ unsubscribed = %s"END" ; when another agent (or another client of the same agent) ; subscribes to the same SMP queue on the server +connDown = %s"DOWN" +; lost connection (e.g. because of Internet connectivity or server is down) + +connUp = %s"UP" +; restored connection + joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] ; `queueInfo` is the same as in out-of-band message, see SMP protocol ; response is `connected` or `error` @@ -225,18 +248,22 @@ binaryMsg = size CRLF msgBody CRLF ; the last CRLF is in addition to CRLF in the size = 1*DIGIT ; size in bytes msgBody = *OCTET ; any content of specified size - safe for binary +messageId = %s"MID" SP agentMsgId + sent = %s"SENT" SP agentMsgId +messageError = %s"MERR" SP agentMsgId SP + message = %s"MSG" SP msgIntegrity SP recipientMeta SP brokerMeta SP senderMeta SP binaryMsg recipientMeta = %s"R=" agentMsgId "," agentTimestamp ; receiving agent message metadata brokerMeta = %s"B=" brokerMsgId "," brokerTimestamp ; broker (server) message metadata senderMeta = %s"S=" agentMsgId "," agentTimestamp ; sending agent message metadata brokerMsgId = encoded brokerTimestamp = -msgIntegrity = ok / messageError +msgIntegrity = ok / msgIntegrityError -messageError = %s"ERR" SP messageErrorType -messageErrorType = skippedMsgErr / badMsgIdErr / badHashErr +msgIntegrityError = %s"ERR" SP msgIntegrityErrorType +msgIntegrityErrorType = skippedMsgErr / badMsgIdErr / badHashErr skippedMsgErr = %s"NO_ID" SP missingFromMsgId SP missingToMsgId badMsgIdErr = %s"ID" SP previousMsgId ; ID is lower than the previous @@ -247,7 +274,6 @@ missingToMsgId = agentMsgId previousMsgId = agentMsgId acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R") -; currently not implemented received = %s"RCVD" SP agentMsgId ; ID assigned by sending agent (in SENT response) ; currently not implemented @@ -261,27 +287,41 @@ error = %s"ERR" SP #### NEW command and INV response -`NEW` command is used to create a connection and an invitation to be sent out-of-band to another protocol user. It should be used by the client of the agent that initiates creating a duplex connection. +`NEW` command is used to create a connection and an invitation to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party). -`INV` response is sent by the agent to the client. +`INV` response is sent by the agent to the client of the initiating party. #### JOIN command -It is used to create a connection and accept the invitation received out-of-band. It should be used by the client of the agent that accepts the connection. +It is used to create a connection and accept the invitation received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). -#### CON notification +#### REQ notification and ACPT command -It is sent by both agents managing duplex connection to their clients once the connection is established and ready to accept client messages. +When the joining party uses `JOIN` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose. + +To continue with the connection the initiating party should use `ACPT` command. + +#### INFO and CON notifications + +After the initiating party proceeds with the connection using `ACPT` command, the joining party will receive `INFO` notification that can be used to identify the initiating party or for any other purpose. + +Once the connection is established and ready to accept client messages, both agents will send `CON` notification to their clients. #### SUB command This command can be used by the client to resume receiving messages from the connection that was created in another TCP/client session. Agent response to this command can be `OK` or `ERR` in case connection does not exist (or can only be used to send connections - e.g. when the reply queue was not created). -#### SEND command and SENT response +#### SEND command and MID, SENT and MERR responses -`SEND` command is used to the client to send messages +`SEND` command is used by the client to send messages. -`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. Message ID in this response is the sequential message number that includes both sent and received messages in the connection. +`MID` notification with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server. + +`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session. + +In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted. + +In case of client disconnecting from the agent, the pending messages will not be sent until the client re-connects to the agent and subscribes to the connection that has pending messages. #### MSG notification @@ -294,6 +334,12 @@ It is sent by the agent to the client when agent receives the message from the S It is sent by the agent to the client when agent receives SMP protocol `END` notification from SMP server. It indicates that another agent has subscribed to the same SMP queue on the server and the server terminated the subscription of the current agent. +#### DOWN and UP notifications + +These notifications are sent when server or network connection is, respectively, `DOWN` or back `UP`. + +All the subscriptions made in the current client session will be automatically resumed when `UP` notification is received. + #### OFF command It is used to suspend the receiving SMP queue - sender will no longer be able to send the messages to the connection, but the recipient can retrieve the remaining messages. Agent response to this command can be `OK` or `ERR`. This command is irreversible. diff --git a/protocol/diagrams/duplex-messaging/duplex-creating.mmd b/protocol/diagrams/duplex-messaging/duplex-creating.mmd index d31a639e0..d8df2d199 100644 --- a/protocol/diagrams/duplex-messaging/duplex-creating.mmd +++ b/protocol/diagrams/duplex-messaging/duplex-creating.mmd @@ -27,11 +27,14 @@ sequenceDiagram note over BA: status: NONE/NEW note over BA, AA: 5. establish Alice's SMP queue - BA ->> AS: SEND: KEY: sender's server key + BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation) note over BA: status: NONE/CONFIRMED activate BA - AS ->> AA: MSG: KEY: sender's server key + AS ->> AA: MSG: Bob's info and
sender server key note over AA: status: CONFIRMED/NONE + AA ->> AS: ACK: confirm message + AA ->> A: REQ: connection request ID
and Bob's info + A ->> AA: ACPT: accept connection request,
send Alice's info AA ->> AS: KEY: secure queue note over AA: status: SECURED/NONE @@ -40,6 +43,7 @@ sequenceDiagram note over BA: status: NONE/ACTIVE AS ->> AA: MSG: HELLO: Alice's agent
knows Bob can send note over AA: status: ACTIVE/NONE + AA ->> AS: ACK: confirm message note over BA, BS: 6. create Bob's SMP queue BA ->> BS: NEW: create SMP queue @@ -51,12 +55,15 @@ sequenceDiagram note over BA: status: PENDING/ACTIVE AS ->> AA: MSG: REPLY: invitation
to connect note over AA: status: ACTIVE/NEW + AA ->> AS: ACK: confirm message - AA ->> BS: SEND: KEY: sender's server key + AA ->> BS: SEND: Alice's info and sender's server key note over AA: status: ACTIVE/CONFIRMED activate AA - BS ->> BA: MSG: KEY: sender's server key + BS ->> BA: MSG: Alice's info and
sender's server key note over BA: status: CONFIRMED/ACTIVE + BA ->> B: INFO: Alice's info + BA ->> BS: ACK: confirm message BA ->> BS: KEY: secure queue note over BA: status: SECURED/ACTIVE @@ -65,6 +72,7 @@ sequenceDiagram note over AA: status: ACTIVE/ACTIVE BS ->> BA: MSG: HELLO: Bob's agent
knows Alice can send note over BA: status: ACTIVE/ACTIVE + BA ->> BS: ACK: confirm message note over A, B: 8. notify users about connection success AA ->> A: CON: connected diff --git a/protocol/diagrams/duplex-messaging/duplex-creating.svg b/protocol/diagrams/duplex-messaging/duplex-creating.svg index c92600134..138935d3b 100644 --- a/protocol/diagrams/duplex-messaging/duplex-creating.svg +++ b/protocol/diagrams/duplex-messaging/duplex-creating.svg @@ -1 +1 @@ -AliceAlice'sagentAlice'sserverBob'sserverBob'sagentBobstatus (receive/send): NONE/NONE1. request connection from agentNEW: createduplex connection2. create Alice's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/NONEINV: invitationto connectstatus: PENDING/NONE3. out-of-band invitationOOB: invitation to connect4. accept connectionJOIN:via invitation infostatus: NONE/NEW5. establish Alice's SMP queueSEND: KEY: sender's server keystatus: NONE/CONFIRMEDMSG: KEY: sender's server keystatus: CONFIRMED/NONEKEY: secure queuestatus: SECURED/NONESEND: HELLO: try sending until successfulstatus: NONE/ACTIVEMSG: HELLO: Alice's agentknows Bob can sendstatus: ACTIVE/NONE6. create Bob's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/ACTIVE7. establish Bob's SMP queueSEND: REPLY: invitation to the connectstatus: PENDING/ACTIVEMSG: REPLY: invitationto connectstatus: ACTIVE/NEWSEND: KEY: sender's server keystatus: ACTIVE/CONFIRMEDMSG: KEY: sender's server keystatus: CONFIRMED/ACTIVEKEY: secure queuestatus: SECURED/ACTIVESEND: HELLO: try sending until successfulstatus: ACTIVE/ACTIVEMSG: HELLO: Bob's agentknows Alice can sendstatus: ACTIVE/ACTIVE8. notify users about connection successCON: connectedCON: connectedAliceAlice'sagentAlice'sserverBob'sserverBob'sagentBob \ No newline at end of file +AliceAlice'sagentAlice'sserverBob'sserverBob'sagentBobstatus (receive/send): NONE/NONE1. request connection from agentNEW: createduplex connection2. create Alice's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/NONEINV: invitationto connectstatus: PENDING/NONE3. out-of-band invitationOOB: invitation to connect4. accept connectionJOIN:via invitation infostatus: NONE/NEW5. establish Alice's SMP queueSEND: Bob's info and sender server key (SMP confirmation)status: NONE/CONFIRMEDMSG: Bob's info andsender server keystatus: CONFIRMED/NONEACK: confirm messageREQ: connection request IDand Bob's infoACPT: accept connection request,send Alice's infoKEY: secure queuestatus: SECURED/NONESEND: HELLO: try sending until successfulstatus: NONE/ACTIVEMSG: HELLO: Alice's agentknows Bob can sendstatus: ACTIVE/NONEACK: confirm message6. create Bob's SMP queueNEW: create SMP queueIDS: SMP queue IDsstatus: NEW/ACTIVE7. establish Bob's SMP queueSEND: REPLY: invitation to the connectstatus: PENDING/ACTIVEMSG: REPLY: invitationto connectstatus: ACTIVE/NEWACK: confirm messageSEND: Alice's info and sender's server keystatus: ACTIVE/CONFIRMEDMSG: Alice's info andsender's server keystatus: CONFIRMED/ACTIVEINFO: Alice's infoACK: confirm messageKEY: secure queuestatus: SECURED/ACTIVESEND: HELLO: try sending until successfulstatus: ACTIVE/ACTIVEMSG: HELLO: Bob's agentknows Alice can sendstatus: ACTIVE/ACTIVEACK: confirm message8. notify users about connection successCON: connectedCON: connectedAliceAlice'sagentAlice'sserverBob'sserverBob'sagentBob \ No newline at end of file diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index a2eadd488..884ae98ec 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -410,7 +410,7 @@ secure = %s"KEY" SP senderKey senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue ``` -`senderKey` is received from the sender as part of the first message - see [Send Message Command](#send-message-command). +`senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command. Once the queue is secured only signed messages can be sent to it. @@ -535,7 +535,8 @@ No further messages should be delivered to unsubscribed transport connection. - transmission has no required signature or queue ID (`NO_AUTH`) - transmission has unexpected credentials (`HAS_AUTH`) - transmission has no required queue ID (`NO_QUEUE`) -- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message command](#send-message-command)). +- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command). +- message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages. - incorrect message body size (`SIZE`). - internal server error (`INTERNAL`).