simplexmq 0.3.2 → 0.4.0
raw patch · 31 files changed
+2223/−1070 lines, 31 filesdep +compositiondep +constraintsdep +direct-sqlitedep ~ansi-terminaldep ~cryptonitedep ~generic-random
Dependencies added: composition, constraints, direct-sqlite, file-embed
Dependency ranges changed: ansi-terminal, cryptonite, generic-random, optparse-applicative, random
Files
- CHANGELOG.md +24/−0
- README.md +38/−1
- apps/smp-agent/Main.hs +1/−11
- apps/smp-server/Main.hs +17/−5
- simplexmq.cabal +39/−21
- src/Simplex/Messaging/Agent.hs +442/−220
- src/Simplex/Messaging/Agent/Client.hs +131/−63
- src/Simplex/Messaging/Agent/Env/SQLite.hs +40/−5
- src/Simplex/Messaging/Agent/Protocol.hs +140/−91
- src/Simplex/Messaging/Agent/RetryInterval.hs +28/−0
- src/Simplex/Messaging/Agent/Store.hs +73/−34
- src/Simplex/Messaging/Agent/Store/SQLite.hs +503/−249
- src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +72/−0
- src/Simplex/Messaging/Agent/Store/SQLite/Schema.hs +0/−167
- src/Simplex/Messaging/Client.hs +30/−20
- src/Simplex/Messaging/Crypto.hs +37/−7
- src/Simplex/Messaging/Parsers.hs +7/−4
- src/Simplex/Messaging/Protocol.hs +2/−0
- src/Simplex/Messaging/Server.hs +13/−5
- src/Simplex/Messaging/Server/Env/STM.hs +3/−1
- src/Simplex/Messaging/Server/MsgStore.hs +3/−1
- src/Simplex/Messaging/Server/MsgStore/STM.hs +12/−8
- src/Simplex/Messaging/Transport.hs +30/−24
- src/Simplex/Messaging/Util.hs +3/−0
- tests/AgentTests.hs +177/−25
- tests/AgentTests/FunctionalAPITests.hs +163/−0
- tests/AgentTests/SQLiteTests.hs +151/−92
- tests/SMPAgentClient.hs +29/−13
- tests/SMPClient.hs +6/−1
- tests/ServerTests.hs +8/−1
- tests/Test.hs +1/−1
CHANGELOG.md view
@@ -1,3 +1,27 @@+# 0.4.0++- SMP server implementation and [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) changes:+ - support 3072 bit RSA key size+ - add SMP queue quotas+ - set default transport block size to 4096 bits+ - allow SMP client to change transport block size during transport connection handshake+- SMP agent implementation and protocol changes:+ - additional connection confirmation step for initiating party+ - automatically resume subscribed duplex connections once transport connection is resumed+ - passing an arbitrary binary information between parties during the duplex connection handshake - can be used to identify parties+ - asynchronous duplex connection handshake - the parties do not have to be online at the same time+ - asynchronous message delivery - the agent does not need transport connection to accept client messages for delivery+ - additional confirmation of message reception from the client to prevent message loss in case of process termination+ - set transport block size to 8192 bits (in the future the agent protocol can allow to have different block sizes for different duplex connections)+ - added client commands and notifications (see [agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md)):+ - `REQ` - the notification about joining party establishing connection+ - `ACPT` - the command to accept connection with the joining party+ - `INFO` - the notification with the information from the initiating party+ - `DOWN`/`UP` - the notifications about losing/resuming the connection+ - `ACK` - the command to confirm that the message reception/processing is complete+ - `MID` - the response to `SEND` confirming that the message is accepted by the agent+ - `MERR` - the notification about permanent message delivery error (e.g., `ERR AUTH` indicating that the queue was removed)+ # 0.3.2 - Support websockets
README.md view
@@ -50,11 +50,48 @@ ## Using SMP server and SMP agent -You can either run SMP server locally or try local SMP agent with the deployed demo server:+You can either run your own SMP server locally or deploy using [Linode StackScript](#deploy-smp-server-on-linode), or try local SMP agent with the deployed demo server: `smp1.simplex.im:5223#pLdiGvm0jD1CMblnov6Edd/391OrYsShw+RgdfR0ChA=` It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.++[<img alt="linode" src="./img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)++## Deploy SMP server on Linode++You can get Linode [free credits](https://www.linode.com/lp/affiliate-referral/?irclickid=02-QkdTEpxyLW0W0EOSREQreUkB2DtzGE2lGTE0&irgwc=1&utm_source=impact) to deploy SMP server.++Deployment on [Linode](https://www.linode.com/) is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:++- Create a Linode account or login with an already existing one.+- Open [SMP server StackScript](https://cloud.linode.com/stackscripts/748014) and click "Deploy New Linode".+- You can optionally configure the following parameters:+ - [SMP Server store log](#SMP-server) flag for queue persistence on server restart (recommended).+ - [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) for attaching server info as tags to Linode (server address, public key hash, version) and adding A record to your 2nd level domain (Note: 2nd level e.g. `example.com` domain should be [created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scope should be read/write access to "linodes" (to update linode tags - you need them), and "domains" (to add A record for the 3rd level domain, e.g. `smp`).+ - Domain name to use instead of Linode ip address, e.g. `smp.example.com`.+- Choose the region and plan according to your requirements (for regular use Shared CPU Nanode should be sufficient).+- Provide ssh key to be able to connect to your Linode via ssh. This step is required if you haven't provided a Linode API token, because you will need to login to your Linode and get a public key hash either from the welcome message or from the file `/etc/opt/simplex/pub_key_hash` on your Linode after SMP server starts.+- Deploy your Linode. After it starts wait for SMP server to start and for tags to appear (if a Linode API token was provided). It may take up to 5 minutes depending on the connection speed on the Linode. Connecting Linode IP address to provided domain name may take some additional time.+- Get `hostname` and `hash` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh. Linode has a good [guide](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) about ssh.+- Great, your own SMP server is ready! Use `address#hash` as SMP server address in the client.++Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.++[<img alt="linode" src="./img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)++## Deploy SMP server on DigitalOcean++You can deploy SMP server using [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:++- Create a DigitalOcean account or login with an already existing one.+- Click 'Create SimpleX server Droplet' button.+- Choose the region and plan according to your requirements (cheapest Regular plan should be sufficient).+- Provide ssh key and confirm Droplet creation. +- SSH to created Droplet (`ssh root@<droplet_ip_address>`) to get SMP server public key hash - either from the welcome message or from `/etc/opt/simplex/pub_key_hash`. DigitalOcean has a good guide on [how to login to Droplet via ssh](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).+- Great, your own SMP server is ready! Use `ip_address#hash` as SMP server address in the client.++Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur. ## SMP server design
apps/smp-agent/Main.hs view
@@ -8,20 +8,10 @@ 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#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="],- rsaKeySize = 2048 `div` 8,- connIdBytes = 12,- tbqSize = 16,- dbFile = "smp-agent.db",- smpCfg = smpDefaultConfig- }+cfg = defaultAgentConfig {smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="]} logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
apps/smp-server/Main.hs view
@@ -29,19 +29,25 @@ 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 { tbqSize = 16,+ msgQueueQuota = 256, queueIdBytes = 12, msgIdBytes = 6, -- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini transports = undefined, storeLog = undefined,+ blockSize = undefined, serverPrivateKey = undefined } @@ -96,9 +102,9 @@ 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@@ -109,6 +115,7 @@ initializeServer :: ServerOpts -> IO ServerConfig initializeServer opts = do+ createDirectoryIfMissing False cfgDir ini <- createIni opts pk <- createKey ini storeLog <- openStoreLog opts ini@@ -138,6 +145,7 @@ storeLogFile :: FilePath, serverKeyFile :: FilePath, serverPort :: ServiceName,+ blockSize :: Int, enableWebsockets :: Bool } @@ -150,7 +158,8 @@ 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 +186,9 @@ \# port: " <> defaultServerPort <> "\n\+ \# block_size: "+ <> show defaultBlockSize+ <> "\n\ \websockets: on\n" pure IniOpts@@ -184,6 +196,7 @@ storeLogFile = defaultStoreLogFile, serverKeyFile = defaultKeyFile, serverPort = defaultServerPort,+ blockSize = defaultBlockSize, enableWebsockets = True } @@ -201,7 +214,6 @@ createKey :: IniOpts -> IO C.FullPrivateKey createKey IniOpts {serverKeyFile} = do- createDirectoryIfMissing True cfgDir (_, pk) <- C.generateKeyPair newKeySize S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk] pure pk@@ -222,7 +234,7 @@ 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}
simplexmq.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack ----- hash: 1f213808753ff701ca8bcdc7b3296d99346fdbe9e0b6fbcfb372f928bcffa525+-- hash: e25d53dc1b12c8bcdf029091182613198be5271348ea19174b6532d820fe1fc9 name: simplexmq-version: 0.3.2+version: 0.4.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and@@ -35,9 +35,10 @@ 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.Schema+ Simplex.Messaging.Agent.Store.SQLite.Migrations Simplex.Messaging.Client Simplex.Messaging.Crypto Simplex.Messaging.Parsers@@ -59,7 +60,7 @@ 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.*+ , ansi-terminal >=0.10 && <0.12 , asn1-encoding ==0.9.* , asn1-types ==0.3.* , async ==2.2.*@@ -67,17 +68,21 @@ , 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.*+ , 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.*- , random ==1.1.*+ , random >=1.1 && <1.3 , simple-logger ==0.1.* , sqlite-simple ==0.4.* , stm ==2.5.*@@ -100,7 +105,7 @@ 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.*+ , ansi-terminal >=0.10 && <0.12 , asn1-encoding ==0.9.* , asn1-types ==0.3.* , async ==2.2.*@@ -108,17 +113,21 @@ , 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.*+ , 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.*- , random ==1.1.*+ , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplexmq , sqlite-simple ==0.4.*@@ -142,7 +151,7 @@ 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.*+ , ansi-terminal >=0.10 && <0.12 , asn1-encoding ==0.9.* , asn1-types ==0.3.* , async ==2.2.*@@ -150,20 +159,24 @@ , 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.*+ , 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.*+ , 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.*- , random ==1.1.*+ , optparse-applicative >=0.15 && <0.17+ , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplexmq , sqlite-simple ==0.4.*@@ -183,6 +196,7 @@ main-is: Test.hs other-modules: AgentTests+ AgentTests.FunctionalAPITests AgentTests.SQLiteTests ProtocolErrorTests ServerTests@@ -195,7 +209,7 @@ build-depends: HUnit ==1.6.* , QuickCheck ==2.14.*- , ansi-terminal ==0.10.*+ , ansi-terminal >=0.10 && <0.12 , asn1-encoding ==0.9.* , asn1-types ==0.3.* , async ==2.2.*@@ -203,11 +217,15 @@ , 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.*+ , 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 , hspec ==2.7.* , hspec-core ==2.7.* , iso8601-time ==0.1.*@@ -215,7 +233,7 @@ , mtl ==2.2.* , network ==3.1.* , network-transport ==0.5.*- , random ==1.1.*+ , random >=1.1 && <1.3 , simple-logger ==0.1.* , simplexmq , sqlite-simple ==0.4.*
src/Simplex/Messaging/Agent.hs view
@@ -1,12 +1,17 @@+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-} -- | -- Module : Simplex.Messaging.Agent@@ -21,10 +26,29 @@ -- -- 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+ AgentClient (..),+ AgentMonad,+ AgentErrorMonad, getSMPAgentClient,- runSMPAgentClient,+ disconnectAgentClient, -- used in tests+ withAgentLock,+ createConnection,+ joinConnection,+ acceptConnection,+ subscribeConnection,+ sendMessage,+ ackMessage,+ suspendConnection,+ deleteConnection, ) where @@ -34,10 +58,16 @@ 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.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@@ -45,16 +75,17 @@ 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, connectSQLiteStore)+import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Client (SMPServerTransmission) import qualified Simplex.Messaging.Crypto as C-import Simplex.Messaging.Protocol (CorrId (..), MsgBody, SenderPublicKey)+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 (race_)+import UnliftIO.Async (Async, async, race_) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -75,19 +106,68 @@ where 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+ liftIO $ putLn h "Welcome to SMP v0.4.0 agent"+ 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` disconnectAgentClient 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 AgentClient+getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent+ where+ runAgent = do+ c <- getAgentClient+ action <- async $ subscriber c `E.finally` disconnectAgentClient c+ pure c {smpSubscriber = action} +disconnectAgentClient :: MonadUnliftIO m => AgentClient -> m ()+disconnectAgentClient c = closeAgentClient c >> logConnection c False++-- |+type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m)++-- | Create SMP agent connection (NEW command)+createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, SMPQueueInfo)+createConnection c = withAgentEnv c $ newConn c ""++-- | Join SMP agent connection (JOIN command)+joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId+joinConnection c = withAgentEnv c .: joinConn c ""++-- | Approve confirmation (LET command)+acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m ()+acceptConnection c = withAgentEnv c .:. acceptConnection' c++-- | Subscribe to receive connection messages (SUB command)+subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()+subscribeConnection c = withAgentEnv c . subscribeConnection' c++-- | Send message to the connection (SEND command)+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++-- | Delete SMP agent connection (DEL command)+deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()+deleteConnection c = withAgentEnv c . deleteConnection' 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+getAgentClient = ask >>= atomically . newAgentClient+ connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m () connectClient h c = race_ (send h c) (receive h c) @@ -97,19 +177,15 @@ 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 c = do- db <- asks $ dbFile . config- s1 <- connectSQLiteStore db- s2 <- connectSQLiteStore db- race_ (subscriber c s1) (client c s2)+runAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()+runAgentClient c = race_ (subscriber c) (client c) receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()-receive h c@AgentClient {rcvQ, sndQ} = forever $ do- (corrId, cAlias, cmdOrErr) <- tGet SClient h+receive h c@AgentClient {rcvQ, subQ} = forever $ do+ (corrId, connId, cmdOrErr) <- tGet SClient h case cmdOrErr of- Right cmd -> write rcvQ (corrId, cAlias, cmd)- Left e -> write sndQ (corrId, cAlias, ERR e)+ Right cmd -> write rcvQ (corrId, connId, cmd)+ Left e -> write subQ (corrId, connId, ERR e) where write :: TBQueue (ATransmission p) -> ATransmission p -> m () write q t = do@@ -117,193 +193,330 @@ 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 logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()-logClient AgentClient {clientId} dir (CorrId corrId, cAlias, cmd) = do- logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, cAlias, 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 :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()-client c@AgentClient {rcvQ, sndQ} st = forever $ do- t@(corrId, cAlias, _) <- atomically $ readTBQueue rcvQ- runExceptT (processCommand c st t) >>= \case- Left e -> atomically $ writeTBQueue sndQ (corrId, cAlias, ERR e)- Right _ -> return ()+client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()+client c@AgentClient {rcvQ, subQ} = forever $ do+ (corrId, connId, cmd) <- atomically $ readTBQueue rcvQ+ withAgentLock c (runExceptT $ processCommand c (connId, cmd))+ >>= atomically . writeTBQueue subQ . \case+ Left e -> (corrId, connId, ERR e)+ Right (connId', resp) -> (corrId, connId', resp) 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+ -- 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 storeError = \case- SEConnNotFound -> CONN UNKNOWN+ SEConnNotFound -> CONN NOT_FOUND SEConnDuplicate -> CONN DUPLICATE+ SEBadConnType CRcv -> CONN SIMPLEX+ SEBadConnType CSnd -> CONN SIMPLEX e -> INTERNAL $ show e -processCommand :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ATransmission 'Client -> m ()-processCommand c@AgentClient {sndQ} st (corrId, connAlias, cmd) =- case cmd of- NEW -> createNewConnection- JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode- SUB -> subscribeConnection connAlias- 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+ 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 <$> sendMessage' c connId msgBody+ ACK msgId -> ackMessage' c connId msgId $> (connId, OK)+ OFF -> suspendConnection' c connId $> (connId, OK)+ DEL -> deleteConnection' c connId $> (connId, OK)++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}+ connId' <- withStore $ \st -> createRcvConn st g cData rq+ addSubscription c rq connId'+ pure (connId', qInfo)++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}+ connId' <- withStore $ \st -> createSndConn st g cData sq+ confirmQueue c sq senderKey cInfo+ activateQueueJoining c connId' sq verifyKey $ retryInterval cfg+ 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- createNewConnection :: m ()- createNewConnection = do- -- TODO create connection alias if not passed- -- make connAlias Maybe?+ createReplyQueue :: m ()+ createReplyQueue = do srv <- getSMPServer- (rq, qInfo) <- newReceiveQueue c srv connAlias- withStore $ createRcvConn st rq- respond $ 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+ (rq, qInfo') <- newRcvQueue c srv+ addSubscription c rq connId+ withStore $ \st -> upgradeSndConnToDuplex st connId rq+ sendControlMessage c sq $ REPLY qInfo' - joinConnection :: SMPQueueInfo -> ReplyMode -> m ()- joinConnection qInfo (ReplyMode replyMode) = do- -- TODO create connection alias if not passed- -- make connAlias Maybe?- (sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias- withStore $ createSndConn st sq- connectToSendQueue c st sq senderKey verifyKey- when (replyMode == On) $ createReplyQueue sq- -- TODO this response is disabled to avoid two responses in terminal client (OK + CON),- -- respond OK+-- | Approve confirmation (LET command) in Reader monad+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+ processConfirmation c rq senderKey+ _ -> throwError $ CMD PROHIBITED - subscribeConnection :: ConnAlias -> m ()- subscribeConnection cAlias =- withStore (getConn st cAlias) >>= \case- SomeConn _ (DuplexConnection _ rq _) -> subscribe rq- SomeConn _ (RcvConnection _ rq) -> subscribe rq- _ -> throwError $ CONN SIMPLEX- where- subscribe rq = subscribeQueue c rq cAlias >> respond' cAlias OK+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 - -- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct- subscribeAll :: m ()- subscribeAll = withStore (getAllConnAliases st) >>= mapM_ subscribeConnection+-- | Subscribe to receive connection messages (SUB command) in Reader monad+subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m ()+subscribeConnection' c connId =+ withStore (`getConn` connId) >>= \case+ 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"+ 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+ subscribeQueue c rq connId+ resumeInterval :: m RetryInterval+ resumeInterval = do+ r <- asks $ retryInterval . config+ pure r {initialInterval = 5_000_000} - sendMessage :: MsgBody -> m ()- sendMessage msgBody =- withStore (getConn st connAlias) >>= \case- SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq- SomeConn _ (SndConnection _ sq) -> sendMsg sq- _ -> throwError $ CONN SIMPLEX+-- | Send message to the connection (SEND command) in Reader monad+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 AgentMsgId+ 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 $ unId msgId where- sendMsg sq = do+ storeSentMsg :: m InternalId+ storeSentMsg = do internalTs <- liftIO getCurrentTime- (internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st sq- let msgStr =- serializeSMPMessage- SMPMessage- { senderMsgId = unSndId internalSndId,- senderTimestamp = internalTs,- previousMsgHash,- agentMessage = A_MSG msgBody- }- msgHash = C.sha256Hash msgStr- withStore $- createSndMsg st sq $- SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash}- sendAgentMessage c sq msgStr- respond $ SENT (unId internalId)+ withStore $ \st -> do+ (internalId, internalSndId, previousMsgHash) <- updateSndIds st connId+ let msgBody =+ serializeSMPMessage+ SMPMessage+ { senderMsgId = unSndId internalSndId,+ senderTimestamp = internalTs,+ previousMsgHash,+ agentMessage = A_MSG msg+ }+ internalHash = C.sha256Hash msgBody+ msgData = SndMsgData {..}+ createSndMsg st connId msgData+ pure internalId - suspendConnection :: m ()- suspendConnection =- withStore (getConn st connAlias) >>= \case- SomeConn _ (DuplexConnection _ rq _) -> suspend rq- SomeConn _ (RcvConnection _ rq) -> suspend rq- _ -> throwError $ CONN SIMPLEX- where- suspend rq = suspendQueue c rq >> respond OK+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 - deleteConnection :: m ()- deleteConnection =- withStore (getConn st connAlias) >>= \case- SomeConn _ (DuplexConnection _ rq _) -> delete rq- SomeConn _ (RcvConnection _ rq) -> delete rq- _ -> delConn- where- delConn = withStore (deleteConn st connAlias) >> respond OK- delete rq = do- deleteQueue c rq- removeSubscription c connAlias- delConn+queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> [PendingMsg] -> m ()+queuePendingMsgs c connId pending =+ atomically $ getPendingMsgQ connId (connMsgQueues c) >>= forM_ pending . writeTQueue - createReplyQueue :: SndQueue -> m ()- createReplyQueue sq = do- srv <- getSMPServer- (rq, qInfo) <- newReceiveQueue c srv connAlias- withStore $ upgradeSndConnToDuplex st connAlias rq- senderTimestamp <- liftIO getCurrentTime- sendAgentMessage c sq . serializeSMPMessage $- SMPMessage- { senderMsgId = 0,- senderTimestamp,- previousMsgHash = "",- agentMessage = REPLY qInfo- }+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 - respond :: ACommand 'Agent -> m ()- respond = respond' connAlias+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 - respond' :: ConnAlias -> ACommand 'Agent -> m ()- respond' cAlias resp = atomically $ writeTBQueue sndQ (corrId, cAlias, resp)+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+ 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+ tryError (sendAgentMessage c sq msgBody) >>= \case+ Left e -> case e of+ SMP SMP.QUOTA -> loop+ SMP {} -> notify connId $ MERR mId e+ CMD {} -> notify connId $ MERR mId e+ _ -> loop+ Right () -> do+ 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) -subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()-subscriber c@AgentClient {msgQ} st = forever $ do- -- TODO this will only process messages and notifications+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 =+ withStore (`getConn` 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` connId) >>= \case+ SomeConn _ (DuplexConnection _ rq _) -> delete rq+ SomeConn _ (RcvConnection _ rq) -> delete rq+ _ -> withStore (`deleteConn` connId)+ where+ delete :: RcvQueue -> m ()+ delete rq = do+ deleteQueue c rq+ removeSubscription c connId+ withStore (`deleteConn` 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+ senderTimestamp <- liftIO getCurrentTime+ sendAgentMessage c sq . serializeSMPMessage $+ SMPMessage+ { senderMsgId = 0,+ senderTimestamp,+ previousMsgHash = "",+ agentMessage+ }++subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()+subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ- runExceptT (processSMPTransmission c st t) >>= \case+ withAgentLock c (runExceptT $ processSMPTransmission c 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- withStore (getRcvConn st srv rId) >>= \case- SomeConn SCDuplex (DuplexConnection _ rq _) -> processSMP SCDuplex rq- SomeConn SCRcv (RcvConnection _ rq) -> processSMP SCRcv rq- _ -> atomically $ writeTBQueue sndQ ("", "", ERR $ CONN SIMPLEX)+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) 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 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 connAlias+ removeSubscription c connId logServer "<--" c srv rId "END" notify END _ -> do@@ -311,22 +524,25 @@ notify . ERR $ BROKER UNEXPECTED where notify :: ACommand 'Agent -> m ()- notify msg = atomically $ writeTBQueue sndQ ("", connAlias, msg)+ notify msg = atomically $ writeTBQueue subQ ("", connId, msg) 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 <KEY>" case status of- New -> do- -- TODO currently it automatically allows whoever sends the confirmation- -- Commands CONF and LET are not supported in v0.2- withStore $ setRcvQueueStatus st rq Confirmed- -- TODO update sender key in the store?- secureQueue c rq senderKey- withStore $ 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 $ REQ confId cInfo+ SCDuplex -> do+ notify $ INFO cInfo+ processConfirmation c rq senderKey+ _ -> prohibited _ -> prohibited helloMsg :: SenderPublicKey -> ByteString -> m ()@@ -336,9 +552,9 @@ Active -> prohibited _ -> do void $ verifyMessage (Just verifyKey) msgBody- withStore $ setRcvQueueActive st rq verifyKey+ withStore $ \st -> setRcvQueueActive st rq verifyKey case cType of- SCDuplex -> notify CON+ SCDuplex -> notifyConnected c connId _ -> pure () replyMsg :: SMPQueueInfo -> m ()@@ -346,42 +562,26 @@ logServer "<--" c srv rId "MSG <REPLY>" case cType of SCRcv -> do- (sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias- withStore $ upgradeRcvConnToDuplex st connAlias sq- connectToSendQueue c st sq senderKey verifyKey- notify CON+ AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId)+ (sq, senderKey, verifyKey) <- newSndQueue qInfo+ withStore $ \st -> upgradeRcvConnToDuplex st connId sq+ confirmQueue c sq senderKey ownConnInfo+ withStore (`removeConfirmations` connId)+ cfg <- asks config+ activateQueueInitiating c connId sq verifyKey $ retryInterval cfg _ -> prohibited 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 <MSG>"- case status of- Active -> do- internalTs <- liftIO getCurrentTime- (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st rq- let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash receivedPrevMsgHash- withStore $- createRcvMsg st rq $- RcvMsgData- { internalId,- internalRcvId,- internalTs,- senderMeta,- brokerMeta,- msgBody,- internalHash = msgHash,- externalPrevSndHash = receivedPrevMsgHash,- msgIntegrity- }- notify- MSG- { recipientMeta = (unId internalId, internalTs),- senderMeta,- brokerMeta,- msgBody,- msgIntegrity- }- _ -> 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 {..}+ withStore $ \st -> createRcvMsg st connId rcvMsg+ notify $ MSG msgMeta msgBody checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash receivedPrevMsgHash@@ -392,16 +592,39 @@ | 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- sendConfirmation c sq senderKey- withStore $ setSndQueueStatus st sq Confirmed- sendHello c sq verifyKey- withStore $ setSndQueueStatus st sq Active+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 -newSendQueue ::- (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> ConnAlias -> m (SndQueue, SenderPublicKey, VerificationKey)-newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do+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 = atomically $ writeTBQueue (subQ c) ("", connId, CON)++newSndQueue ::+ (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey)+newSndQueue (SMPQueueInfo smpServer senderId encryptKey) = do size <- asks $ rsaKeySize . config (senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size (verifyKey, signKey) <- liftIO $ C.generateKeyPair size@@ -409,7 +632,6 @@ SndQueue { server = smpServer, sndId = senderId,- connAlias, sndPrivateKey, encryptKey, signKey,
src/Simplex/Messaging/Agent/Client.hs view
@@ -11,11 +11,13 @@ ( AgentClient (..), newAgentClient, AgentMonad,- getSMPServerClient,- closeSMPServerClients,- newReceiveQueue,+ withAgentLock,+ closeAgentClient,+ newRcvQueue, subscribeQueue,+ addSubscription, sendConfirmation,+ RetryInterval (..), sendHello, secureQueue, sendAgentMessage,@@ -27,9 +29,14 @@ logServer, removeSubscription, cryptoError,+ addActivation,+ getActivation,+ removeActivation, ) where +import Control.Concurrent.Async (Async, async, uninterruptibleCancel)+import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except import Control.Monad.IO.Unlift@@ -40,44 +47,62 @@ 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 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 ConnAlias)),- subscrConns :: TVar (Map ConnAlias SMPServer),- clientId :: Int+ 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,+ smpSubscriber :: Async (),+ lock :: TMVar () } -newAgentClient :: TVar Int -> AgentConfig -> STM AgentClient-newAgentClient cc AgentConfig {tbqSize} = do- rcvQ <- newTBQueue tbqSize- sndQ <- newTBQueue tbqSize- msgQ <- newTBQueue tbqSize+newAgentClient :: Env -> STM AgentClient+newAgentClient 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}+ 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, 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) getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient@@ -95,34 +120,77 @@ 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 ConnAlias))- 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 :: ConnAlias -> IO ()- notifySub connAlias = atomically $ writeTBQueue (sndQ c) ("", connAlias, 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 :) -closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m ()-closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient+ 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+ cancelActions $ activations c+ cancelActions $ reconnections c+ cancelActions $ connMsgDeliveries c+ cancelActions $ srvMsgDeliveries c++closeSMPServerClients :: AgentClient -> IO ()+closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ closeSMPClient++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 = (getSMPServerClient c srv >>= action) `catchError` logServerError@@ -158,8 +226,8 @@ SMPTransportError e -> BROKER $ TRANSPORT e e -> INTERNAL $ show e -newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> ConnAlias -> m (RcvQueue, SMPQueueInfo)-newReceiveQueue c srv connAlias = 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"@@ -170,45 +238,51 @@ RcvQueue { server = srv, rcvId,- connAlias, rcvPrivateKey, sndId = Just sId,- sndKey = Nothing, decryptKey, verifyKey = Nothing, status = New }- addSubscription c rq connAlias return (rq, SMPQueueInfo srv sId encryptKey) -subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnAlias -> m ()-subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do+subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()+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 -> ConnAlias -> m ()-addSubscription c RcvQueue {server} connAlias = atomically $ do- modifyTVar (subscrConns c) $ M.insert connAlias server+addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m ()+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 ConnAlias) -> Set ConnAlias- addSub (Just cs) = S.insert connAlias cs- addSub _ = S.singleton connAlias+ 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 -> ConnAlias -> m ()-removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do+removeSubscription :: AgentMonad m => AgentClient -> ConnId -> m ()+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 ConnAlias -> Maybe (Set ConnAlias)+ delSub :: Map ConnId RcvQueue -> Maybe (Map ConnId RcvQueue) delSub cs =- let cs' = S.delete connAlias 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++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]@@ -219,20 +293,23 @@ 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 <KEY>" $ \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 =+sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m ()+sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey ri = withLogSMP_ c server sndId "SEND <HELLO> (retrying)" $ \smp -> do msg <- mkHello smp $ AckMode On- liftSMP $ send 8 100000 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@@ -244,15 +321,6 @@ previousMsgHash = "", agentMessage = HELLO verifyKey ackMode }-- send :: Int -> Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO ()- send 0 _ _ _ = throwE $ SMPServerError AUTH- send retry 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- e -> throwE e secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey =
src/Simplex/Messaging/Agent/Env/SQLite.hs view
@@ -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@@ -11,7 +12,9 @@ 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 import System.Random (StdGen, newStdGen) import UnliftIO.STM@@ -23,11 +26,43 @@ connIdBytes :: Int, tbqSize :: Natural, dbFile :: FilePath,- smpCfg :: SMPClientConfig+ dbPoolSize :: Int,+ smpCfg :: SMPClientConfig,+ retryInterval :: RetryInterval,+ reconnectInterval :: RetryInterval } +minute :: Int+minute = 60_000_000++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+ },+ reconnectInterval =+ RetryInterval+ { initialInterval = 1_000_000,+ increaseAfter = 10_000_000,+ maxInterval = 10_000_000+ }+ }+ data Env = Env { config :: AgentConfig,+ store :: SQLiteStore, idsDrg :: TVar ChaChaDRG, clientCounter :: TVar Int, reservedMsgSize :: Int,@@ -35,15 +70,15 @@ } newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env-newSMPAgentEnv config = do+newSMPAgentEnv cfg = do idsDrg <- newTVarIO =<< drgNew- _ <- createSQLiteStore $ dbFile config+ 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)
src/Simplex/Messaging/Agent/Protocol.hs view
@@ -1,14 +1,17 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} -- |@@ -25,9 +28,12 @@ -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md module Simplex.Messaging.Agent.Protocol ( -- * SMP agent protocol types+ ConnInfo, ACommand (..), AParty (..), SAParty (..),+ MsgHash,+ MsgMeta (..), SMPMessage (..), AMessage (..), SMPServer (..),@@ -40,8 +46,10 @@ ATransmission, ATransmissionOrError, ARawTransmission,- ConnAlias,- ReplyMode (..),+ ConnId,+ ConfirmationId,+ IntroId,+ InvitationId, AckMode (..), OnOff (..), MsgIntegrity (..),@@ -51,6 +59,8 @@ VerificationKey, EncryptionKey, DecryptionKey,+ ACorrId,+ AgentMsgId, -- * Parse and serialize serializeCommand,@@ -91,12 +101,11 @@ import Data.Typeable () import GHC.Generics (Generic) import Generic.Random (genericArbitraryU)-import Network.Socket+import Network.Socket (HostName, ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol- ( CorrId (..),- ErrorType,+ ( ErrorType, MsgBody, MsgId, SenderPublicKey,@@ -112,11 +121,13 @@ type ARawTransmission = (ByteString, ByteString, ByteString) -- | Parsed SMP agent protocol transmission.-type ATransmission p = (CorrId, ConnAlias, ACommand p)+type ATransmission p = (ACorrId, ConnId, ACommand p) -- | SMP agent protocol transmission or transmission error.-type ATransmissionOrError p = (CorrId, ConnAlias, Either AgentErrorType (ACommand p))+type ATransmissionOrError p = (ACorrId, ConnId, Either AgentErrorType (ACommand p)) +type ACorrId = ByteString+ -- | SMP agent protocol participants. data AParty = Agent | Client deriving (Eq, Show)@@ -139,31 +150,29 @@ deriving instance Show ACmd +type ConnInfo = ByteString+ -- | Parameterized type for SMP agent protocol commands and responses from all participants. 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+ 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- -- TODO currently it automatically allows whoever sends the confirmation- -- CONF :: OtherPartyId -> ACommand Agent- -- LET :: OtherPartyId -> ACommand Client SUB :: ACommand Client- SUBALL :: ACommand Client -- TODO should be moved to chat protocol - hack for subscribing to all 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 ::- { recipientMeta :: (AgentMsgId, UTCTime),- brokerMeta :: (MsgId, UTCTime),- senderMeta :: (AgentMsgId, UTCTime),- msgIntegrity :: MsgIntegrity,- msgBody :: MsgBody- } ->- ACommand Agent- -- ACK :: AgentMsgId -> ACommand Client+ MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent+ MSG :: MsgMeta -> MsgBody -> ACommand Agent+ ACK :: AgentMsgId -> ACommand Client -- RCVD :: AgentMsgId -> ACommand Agent OFF :: ACommand Client DEL :: ACommand Client@@ -174,11 +183,27 @@ deriving instance Show (ACommand p) +type MsgHash = ByteString++-- | 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 -- (see <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message SMP protocol>)- 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 <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents SMP agent protocol>) SMPMessage@@ -187,7 +212,7 @@ -- | 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 }@@ -210,12 +235,10 @@ 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 =@@ -230,7 +253,7 @@ -- | 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@@ -248,9 +271,7 @@ where hello = HELLO <$> C.pubKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP- a_msg = do- size :: Int <- A.decimal <* A.endOfLine- A_MSG <$> A.take size <* A.endOfLine+ a_msg = A_MSG <$> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) -- | SMP queue information parser.@@ -262,7 +283,7 @@ 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) @@ -270,7 +291,7 @@ 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_MSG body -> "MSG " <> serializeBinary body <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString@@ -291,11 +312,17 @@ deriving (Eq, Ord, Show) instance IsString SMPServer where- fromString = parseString . parseAll $ smpServerP+ fromString = parseString $ parseAll smpServerP -- | SMP agent connection alias.-type ConnAlias = ByteString+type ConnId = ByteString +type ConfirmationId = ByteString++type IntroId = ByteString++type InvitationId = ByteString+ -- | Connection modes. data OnOff = On | Off deriving (Eq, Show, Read) @@ -308,9 +335,6 @@ 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 @@ -318,7 +342,7 @@ 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@@ -369,11 +393,11 @@ -- | SMP agent protocol command or response error. data CommandErrorType- = -- | command is prohibited+ = -- | command is prohibited in this context PROHIBITED | -- | command syntax is invalid SYNTAX- | -- | connection alias is required with this command+ | -- | entity ID is required with this command NO_CONN | -- | message size is not correct (no terminating space) SIZE@@ -384,7 +408,7 @@ -- | Connection error. data ConnectionErrorType = -- | connection alias is not in the database- UNKNOWN+ NOT_FOUND | -- | connection alias already exists DUPLICATE | -- | connection is simplex, but operation requires another queue@@ -427,18 +451,25 @@ instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU --- | AMP agent command and response parser+-- | SMP agent command and response parser commandP :: Parser ACmd commandP = "NEW" $> ACmd SClient NEW <|> "INV " *> invResp <|> "JOIN " *> joinCmd+ <|> "REQ " *> reqCmd+ <|> "ACPT " *> acptCmd+ <|> "INFO " *> infoCmd <|> "SUB" $> ACmd SClient SUB- <|> "SUBALL" $> ACmd SClient SUBALL -- TODO remove - hack for subscribing to all <|> "END" $> ACmd SAgent END+ <|> "DOWN" $> ACmd SAgent DOWN+ <|> "UP" $> ACmd SAgent UP <|> "SEND " *> sendCmd+ <|> "MID " *> msgIdResp <|> "SENT " *> sentResp+ <|> "MERR " *> msgErrResp <|> "MSG " *> message+ <|> "ACK " *> ackCmd <|> "OFF" $> ACmd SClient OFF <|> "DEL" $> ACmd SClient DEL <|> "ERR " *> agentError@@ -446,18 +477,23 @@ <|> "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)+ 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+ msgIdResp = ACmd SAgent . MID <$> A.decimal sentResp = ACmd SAgent . SENT <$> A.decimal- 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}- replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On)- partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space+ 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+ broker <- " B=" *> partyMeta base64P+ sender <- " S=" *> partyMeta A.decimal+ pure MsgMeta {integrity, recipient, broker, sender}+ partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P agentError = ACmd SAgent . ERR <$> agentErrorTypeP -- | Message integrity validation result parser.@@ -478,33 +514,37 @@ serializeCommand = \case NEW -> "NEW" INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo- JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode+ JOIN qInfo cInfo -> "JOIN " <> serializeSmpQueueInfo qInfo <> " " <> serializeBinary cInfo+ REQ confId cInfo -> "REQ " <> confId <> " " <> serializeBinary cInfo+ ACPT confId cInfo -> "ACPT " <> confId <> " " <> serializeBinary cInfo+ INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB"- SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all END -> "END"- SEND msgBody -> "SEND " <> serializeMsg msgBody+ DOWN -> "DOWN"+ UP -> "UP"+ SEND msgBody -> "SEND " <> serializeBinary msgBody+ MID mId -> "MID " <> bshow mId 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- ]+ 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" 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+ 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@@ -535,14 +575,19 @@ 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 ()-tPutRaw h (corrId, connAlias, command) = do+tPutRaw h (corrId, entity, command) = do putLn h corrId- putLn h connAlias+ putLn h entity putLn h command -- | Receive raw (unparsed) SMP agent protocol transmission from TCP connection.@@ -551,7 +596,7 @@ -- | Send SMP agent protocol command (or response) to TCP connection. tPut :: (Transport c, MonadIO m) => c -> ATransmission p -> m ()-tPut h (CorrId corrId, connAlias, command) =+tPut h (corrId, connAlias, command) = liftIO $ tPutRaw h (corrId, connAlias, serializeCommand command) -- | Receive client and agent transmissions from TCP connection.@@ -559,38 +604,42 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody where tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)- tParseLoadBody t@(corrId, connAlias, command) = do- let cmd = parseCommand command >>= fromParty >>= tConnAlias t+ tParseLoadBody t@(corrId, connId, command) = do+ let cmd = parseCommand command >>= fromParty >>= tConnId t fullCmd <- either (return . Left) cmdWithMsgBody cmd- return (CorrId corrId, connAlias, fullCmd)+ return (corrId, connId, fullCmd) fromParty :: ACmd -> Either AgentErrorType (ACommand p) fromParty (ACmd (p :: p1) cmd) = case testEquality party p of Just Refl -> Right cmd _ -> Left $ CMD PROHIBITED - tConnAlias :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p)- tConnAlias (_, connAlias, _) cmd = case cmd of- -- NEW and JOIN have optional connAlias+ 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- -- ERROR response does not always have connAlias+ JOIN {} -> Right cmd+ -- ERROR response does not always have connId ERR _ -> Right cmd- -- other responses must have connAlias+ -- other responses must have connId _- | B.null connAlias -> Left $ CMD NO_CONN+ | B.null connId -> Left $ CMD NO_CONN | otherwise -> 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- cmd -> return $ Right cmd+ SEND body -> SEND <$$> getBody body+ MSG msgMeta body -> MSG msgMeta <$$> getBody body+ JOIN qInfo cInfo -> JOIN qInfo <$$> 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 -- 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
+ src/Simplex/Messaging/Agent/RetryInterval.hs view
@@ -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
src/Simplex/Messaging/Agent/Store.hs view
@@ -9,7 +9,9 @@ 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)@@ -30,26 +32,36 @@ -- | 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 ()+ updateSignKey :: s -> SndQueue -> SignatureKey -> m () - updateSndIds :: s -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash)- createSndMsg :: s -> SndQueue -> SndMsgData -> m ()+ -- Confirmations+ createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId+ acceptConfirmation :: s -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation+ getAcceptedConfirmation :: s -> ConnId -> m AcceptedConfirmation+ removeConfirmations :: s -> ConnId -> m () - getMsg :: s -> ConnAlias -> InternalId -> m Msg+ -- 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 ()+ updateSndMsgStatus :: s -> ConnId -> InternalId -> SndMsgStatus -> m ()+ 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 @@ -57,10 +69,8 @@ data RcvQueue = RcvQueue { server :: SMPServer, rcvId :: SMP.RecipientId,- connAlias :: ConnAlias, rcvPrivateKey :: RecipientPrivateKey, sndId :: Maybe SMP.SenderId,- sndKey :: Maybe SenderPublicKey, decryptKey :: DecryptionKey, verifyKey :: Maybe VerificationKey, status :: QueueStatus@@ -71,7 +81,6 @@ data SndQueue = SndQueue { server :: SMPServer, sndId :: SMP.SenderId,- connAlias :: ConnAlias, sndPrivateKey :: SenderPrivateKey, encryptKey :: EncryptionKey, signKey :: SignatureKey,@@ -95,9 +104,9 @@ -- - 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) @@ -134,10 +143,27 @@ deriving instance Show SomeConn --- * Message integrity validation types+newtype ConnData = ConnData {connId :: ConnId}+ deriving (Eq, Show) -type MsgHash = ByteString+-- * 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+ -- | Corresponds to `last_external_snd_msg_id` in `connections` table type PrevExternalSndId = Int64 @@ -152,15 +178,11 @@ -- * 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@@ -168,9 +190,20 @@ 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+ -- * Message types -- | A message in either direction that is stored by the agent.@@ -241,9 +274,9 @@ newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show) data SndMsgStatus- = Created- | Sent- | Delivered+ = SndMsgCreated+ | SndMsgSent+ | SndMsgDelivered deriving (Eq, Show) type SentTs = UTCTime@@ -252,7 +285,7 @@ -- | 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)@@ -276,6 +309,8 @@ 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.@@ -283,6 +318,10 @@ | -- | 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+ | -- | 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.
src/Simplex/Messaging/Agent/Store/SQLite.hs view
@@ -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 #-} @@ -18,19 +20,25 @@ ( SQLiteStore (..), createSQLiteStore, connectSQLiteStore,+ withConnection,+ withTransaction,+ fromTextField_, ) where import Control.Concurrent (threadDelay)-import Control.Monad (when)-import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO))+import Control.Concurrent.STM+import Control.Exception (bracket)+import Control.Monad.Except import Control.Monad.IO.Unlift (MonadUnliftIO)-import Data.Bifunctor (first)+import Crypto.Random (ChaChaDRG, randomBytesGenerate)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 (encode)+import Data.Char (toLower) 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) import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.FromField@@ -41,105 +49,150 @@ import Network.Socket (ServiceName) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store-import Simplex.Messaging.Agent.Store.SQLite.Schema (createSchema)+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.Exit (ExitCode (ExitFailure), exitWith)+import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)+import System.Exit (exitFailure) import System.FilePath (takeDirectory)+import System.IO (hFlush, stdout) import Text.Read (readMaybe)-import UnliftIO.Directory (createDirectoryIfMissing) import qualified UnliftIO.Exception as E -- * SQLite Store implementation data SQLiteStore = SQLiteStore { dbFilePath :: FilePath,- dbConn :: DB.Connection+ dbConnPool :: TBQueue DB.Connection,+ dbNew :: Bool } -createSQLiteStore :: MonadUnliftIO m => FilePath -> m SQLiteStore-createSQLiteStore dbFilePath = do+createSQLiteStore :: FilePath -> Int -> [Migration] -> IO SQLiteStore+createSQLiteStore dbFilePath poolSize migrations = do let dbDir = takeDirectory dbFilePath createDirectoryIfMissing False dbDir- store <- connectSQLiteStore dbFilePath- compileOptions <- liftIO (DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]])- let threadsafeOption = find (isPrefixOf "THREADSAFE=") (concat compileOptions)- liftIO $ case threadsafeOption of- Just "THREADSAFE=0" -> do- putStrLn "SQLite compiled with not threadsafe code, continue (y/n):"- s <- getLine- when (s /= "y") (exitWith $ ExitFailure 2)+ 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 ()- liftIO . createSchema $ dbConn store- return store -connectSQLiteStore :: MonadUnliftIO m => FilePath -> m SQLiteStore-connectSQLiteStore dbFilePath = do- dbConn <- liftIO $ DB.open dbFilePath- liftIO $- DB.execute_- dbConn- [sql|- PRAGMA foreign_keys = ON;- PRAGMA journal_mode = WAL;- |]- return SQLiteStore {dbFilePath, dbConn}+migrateSchema :: SQLiteStore -> [Migration] -> IO ()+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 st) $ do+ confirmOrExit "The app has a newer version than the database - it will be backed up and upgraded."+ let f = dbFilePath st+ copyFile f (f <> ".bak")+ Migrations.run db ms -checkDuplicate :: (MonadUnliftIO m, MonadError StoreError m) => IO () -> m ()-checkDuplicate action = liftIOEither $ first handleError <$> E.try action- where- handleError :: SQLError -> StoreError- handleError e- | DB.sqlError e == DB.ErrorConstraint = SEConnDuplicate- | otherwise = SEInternal $ bshow e+confirmOrExit :: String -> IO ()+confirmOrExit s = do+ putStrLn s+ putStr "Continue (y/N): "+ hFlush stdout+ ok <- getLine+ when (map toLower ok /= "y") exitFailure -withTransaction :: forall a. DB.Connection -> IO a -> IO a-withTransaction db a = loop 100 100_000+connectSQLiteStore :: FilePath -> Int -> IO SQLiteStore+connectSQLiteStore dbFilePath poolSize = do+ dbNew <- not <$> doesFileExist dbFilePath+ 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)++handleSQLError :: StoreError -> SQLError -> StoreError+handleSQLError err e+ | DB.sqlError e == DB.ErrorConstraint = err+ | otherwise = SEInternal $ bshow e++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 -> RcvQueue -> m ()- createRcvConn SQLiteStore {dbConn} q@RcvQueue {server} =- checkDuplicate $- withTransaction dbConn $ do- upsertServer_ dbConn server- insertRcvQueue_ dbConn q- insertRcvConnection_ dbConn q+ createRcvConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId+ 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 st $ \db ->+ getConnId_ db gVar cData >>= traverse (create db)+ where+ 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 -> SndQueue -> m ()- createSndConn SQLiteStore {dbConn} q@SndQueue {server} =- checkDuplicate $- withTransaction dbConn $ do- upsertServer_ dbConn server- insertSndQueue_ dbConn q- insertSndConnection_ dbConn q+ createSndConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId+ 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 st $ \db ->+ getConnId_ db gVar cData >>= traverse (create db)+ where+ 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 -> ConnAlias -> m SomeConn- getConn SQLiteStore {dbConn} connAlias =- liftIOEither . withTransaction dbConn $- getConn_ dbConn connAlias+ getConn :: SQLiteStore -> ConnId -> m SomeConn+ getConn st connId =+ liftIOEither . withTransaction st $ \db ->+ getConn_ db connId - getAllConnAliases :: SQLiteStore -> m [ConnAlias]- getAllConnAliases SQLiteStore {dbConn} =- liftIO $ do- r <- DB.query_ dbConn "SELECT conn_alias FROM connections;" :: IO [[ConnAlias]]- return (concat r)+ getAllConnIds :: SQLiteStore -> m [ConnId]+ getAllConnIds st =+ liftIO . withTransaction st $ \db ->+ 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@@ -147,47 +200,47 @@ |] [":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId] >>= \case- [Only connAlias] -> getConn_ dbConn connAlias+ [Only connId] -> getConn_ db connId _ -> pure $ Left SEConnNotFound - deleteConn :: SQLiteStore -> ConnAlias -> m ()- deleteConn SQLiteStore {dbConn} connAlias =- liftIO $+ deleteConn :: SQLiteStore -> ConnId -> m ()+ deleteConn st connId =+ liftIO . withTransaction st $ \db -> DB.executeNamed- dbConn+ db "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} =- liftIOEither . withTransaction dbConn $- getConn_ dbConn connAlias >>= \case- Right (SomeConn SCRcv (RcvConnection _ _)) -> do- upsertServer_ dbConn server- insertSndQueue_ dbConn sq- updateConnWithSndQueue_ dbConn connAlias sq+ upgradeRcvConnToDuplex :: SQLiteStore -> ConnId -> SndQueue -> m ()+ upgradeRcvConnToDuplex st connId sq@SndQueue {server} =+ liftIOEither . withTransaction st $ \db ->+ getConn_ db connId >>= \case+ Right (SomeConn _ RcvConnection {}) -> do+ 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 -> ConnAlias -> RcvQueue -> m ()- upgradeSndConnToDuplex SQLiteStore {dbConn} connAlias rq@RcvQueue {server} =- liftIOEither . withTransaction dbConn $- getConn_ dbConn connAlias >>= \case- Right (SomeConn SCSnd (SndConnection _ _)) -> do- upsertServer_ dbConn server- insertRcvQueue_ dbConn rq- updateConnWithRcvQueue_ dbConn connAlias rq+ upgradeSndConnToDuplex :: SQLiteStore -> ConnId -> RcvQueue -> m ()+ upgradeSndConnToDuplex st connId rq@RcvQueue {server} =+ liftIOEither . withTransaction st $ \db ->+ getConn_ db connId >>= \case+ Right (SomeConn _ SndConnection {}) -> do+ 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 =- -- ? throw error if queue doesn't exist?- liftIO $+ setRcvQueueStatus st RcvQueue {rcvId, server = SMPServer {host, port}} status =+ -- ? throw error if queue does not exist?+ liftIO . withTransaction st $ \db -> DB.executeNamed- dbConn+ db [sql| UPDATE rcv_queues SET status = :status@@ -196,11 +249,11 @@ [":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 =- -- ? throw error if queue doesn't exist?- liftIO $+ setRcvQueueActive st RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey =+ -- ? throw error if queue does not exist?+ liftIO . withTransaction st $ \db -> DB.executeNamed- dbConn+ db [sql| UPDATE rcv_queues SET verify_key = :verify_key, status = :status@@ -214,11 +267,11 @@ ] setSndQueueStatus :: SQLiteStore -> SndQueue -> QueueStatus -> m ()- setSndQueueStatus SQLiteStore {dbConn} SndQueue {sndId, server = SMPServer {host, port}} status =- -- ? throw error if queue doesn't exist?- liftIO $+ setSndQueueStatus st SndQueue {sndId, server = SMPServer {host, port}} status =+ -- ? throw error if queue does not exist?+ liftIO . withTransaction st $ \db -> DB.executeNamed- dbConn+ db [sql| UPDATE snd_queues SET status = :status@@ -226,41 +279,194 @@ |] [":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] - updateRcvIds :: SQLiteStore -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)- updateRcvIds SQLiteStore {dbConn} RcvQueue {connAlias} =- liftIO . withTransaction dbConn $ do- (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ dbConn connAlias+ updateSignKey :: SQLiteStore -> SndQueue -> SignatureKey -> m ()+ updateSignKey st SndQueue {sndId, server = SMPServer {host, port}} signatureKey =+ liftIO . withTransaction 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 . withTransaction 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 . withTransaction 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+ (lastInternalId, lastInternalRcvId, lastExternalSndId, lastRcvHash) <- retrieveLastIdsAndHashRcv_ db connId let internalId = InternalId $ unId lastInternalId + 1 internalRcvId = InternalRcvId $ unRcvId lastInternalRcvId + 1- updateLastIdsRcv_ dbConn connAlias internalId internalRcvId+ updateLastIdsRcv_ db connId internalId internalRcvId pure (internalId, internalRcvId, lastExternalSndId, lastRcvHash) - createRcvMsg :: SQLiteStore -> RcvQueue -> RcvMsgData -> m ()- createRcvMsg SQLiteStore {dbConn} RcvQueue {connAlias} rcvMsgData =- liftIO . withTransaction dbConn $ do- insertRcvMsgBase_ dbConn connAlias rcvMsgData- insertRcvMsgDetails_ dbConn connAlias rcvMsgData- updateHashRcv_ dbConn connAlias rcvMsgData+ createRcvMsg :: SQLiteStore -> ConnId -> RcvMsgData -> m ()+ createRcvMsg st connId rcvMsgData =+ liftIO . withTransaction st $ \db -> do+ insertRcvMsgBase_ db connId rcvMsgData+ insertRcvMsgDetails_ db connId rcvMsgData+ updateHashRcv_ db connId rcvMsgData - updateSndIds :: SQLiteStore -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash)- updateSndIds SQLiteStore {dbConn} SndQueue {connAlias} =- liftIO . withTransaction dbConn $ do- (lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ dbConn connAlias+ updateSndIds :: SQLiteStore -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)+ 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 connAlias internalId internalSndId+ updateLastIdsSnd_ db connId internalId internalSndId pure (internalId, internalSndId, prevSndHash) - createSndMsg :: SQLiteStore -> SndQueue -> SndMsgData -> m ()- createSndMsg SQLiteStore {dbConn} SndQueue {connAlias} sndMsgData =- liftIO . withTransaction dbConn $ do- insertSndMsgBase_ dbConn connAlias sndMsgData- insertSndMsgDetails_ dbConn connAlias sndMsgData- updateHashSnd_ dbConn connAlias sndMsgData+ createSndMsg :: SQLiteStore -> ConnId -> SndMsgData -> m ()+ createSndMsg st connId sndMsgData =+ liftIO . withTransaction st $ \db -> do+ insertSndMsgBase_ db connId sndMsgData+ insertSndMsgDetails_ db connId sndMsgData+ updateHashSnd_ db connId sndMsgData - getMsg :: SQLiteStore -> ConnAlias -> InternalId -> m Msg- getMsg _st _connAlias _id = throwError SENotImplemented+ 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 _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 -- ? replace with ToField? - it's easy to forget to use this@@ -273,7 +479,7 @@ 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 @@ -295,13 +501,16 @@ instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP -fromFieldToReadable_ :: forall a. (Read a, E.Typeable a) => Field -> Ok a-fromFieldToReadable_ = \case+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 -}@@ -333,49 +542,49 @@ -- * 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 [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_, ":rcv_id" := rcvId,- ":conn_alias" := connAlias,+ ":conn_alias" := connId, ":rcv_private_key" := rcvPrivateKey, ":snd_id" := sndId,- ":snd_key" := sndKey, ":decrypt_key" := decryptKey, ":verify_key" := verifyKey, ":status" := status ] -insertRcvConnection_ :: DB.Connection -> RcvQueue -> IO ()-insertRcvConnection_ dbConn RcvQueue {server, rcvId, connAlias} = do+insertRcvConnection_ :: DB.Connection -> ConnData -> RcvQueue -> IO ()+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,- 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,- 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" := connAlias, ":rcv_host" := host server, ":rcv_port" := port_, ":rcv_id" := rcvId]+ [ ":conn_alias" := connId,+ ":rcv_host" := host server,+ ":rcv_port" := port_,+ ":rcv_id" := rcvId+ ] -- * 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@@ -388,85 +597,94 @@ [ ":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} 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, 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, 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+ ] -- * 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 conn_alias FROM connections WHERE conn_alias = ?;" (Only connId')+ where+ connData [Only connId] = Just ConnData {connId}+ 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,- q.snd_id, q.snd_key, q.decrypt_key, q.verify_key, q.status+ SELECT s.key_hash, q.host, q.port, q.rcv_id, q.rcv_private_key,+ 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 = :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, 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 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@@ -475,12 +693,12 @@ 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@@ -489,12 +707,12 @@ 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@@ -503,11 +721,11 @@ 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|@@ -518,30 +736,32 @@ |] [ ":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 {msgMeta, msgBody, internalRcvId} = do+ let MsgMeta {recipient = (internalId, internalTs)} = msgMeta DB.executeNamed 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" := connAlias,+ [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_rcv_id" := internalRcvId,- ":body" := decodeUtf8 msgBody+ ":msg_body" := msgBody ] -insertRcvMsgDetails_ :: DB.Connection -> ConnAlias -> RcvMsgData -> IO ()-insertRcvMsgDetails_ dbConn connAlias RcvMsgData {..} =+insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()+insertRcvMsgDetails_ dbConn connId RcvMsgData {msgMeta, internalRcvId, internalHash, externalPrevSndHash} = do+ let MsgMeta {integrity, recipient, sender, broker} = msgMeta DB.executeNamed dbConn [sql|@@ -554,21 +774,21 @@ :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,- ":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 -> ConnAlias -> RcvMsgData -> IO ()-updateHashRcv_ dbConn connAlias RcvMsgData {..} =+updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()+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@@ -579,16 +799,16 @@ 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" := 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@@ -597,11 +817,11 @@ 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|@@ -612,47 +832,48 @@ |] [ ":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| 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" := connAlias,+ [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_snd_id" := internalSndId,- ":body" := decodeUtf8 msgBody+ ":msg_body" := 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| 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" := connAlias,+ [ ":conn_alias" := connId, ":internal_snd_id" := internalSndId, ":internal_id" := internalId,- ":snd_status" := Created,- ":internal_hash" := internalHash+ ":snd_status" := SndMsgCreated,+ ":internal_hash" := internalHash,+ ":previous_msg_hash" := previousMsgHash ] -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@@ -663,6 +884,39 @@ 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 ]++-- 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)
+ src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}++module Simplex.Messaging.Agent.Store.SQLite.Migrations+ ( Migration (..),+ app,+ initialize,+ get,+ run,+ )+where++import Control.Monad (forM_)+import Data.FileEmbed (embedDir, makeRelativeToProject)+import Data.Function (on)+import Data.List (intercalate, sortBy)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8)+import Data.Time.Clock (getCurrentTime)+import Database.SQLite.Simple (Connection, Only (..))+import qualified Database.SQLite.Simple as DB+import Database.SQLite.Simple.QQ (sql)+import qualified Database.SQLite3 as SQLite3+import System.FilePath (takeBaseName, takeExtension)++data Migration = Migration {name :: String, up :: Text}+ deriving (Show)++-- | The list of migrations in ascending order by date+app :: [Migration]+app =+ sortBy (compare `on` name) . map migration . filter sqlFile $+ $(makeRelativeToProject "migrations" >>= embedDir)+ where+ sqlFile (file, _) = takeExtension file == ".sql"+ migration (file, qStr) = Migration {name = takeBaseName file, up = decodeUtf8 qStr}++get :: Connection -> [Migration] -> IO (Either String [Migration])+get conn migrations =+ migrationsToRun migrations . map fromOnly+ <$> DB.query_ conn "SELECT name FROM migrations ORDER BY name ASC;"++run :: Connection -> [Migration] -> IO ()+run conn ms = DB.withImmediateTransaction conn . forM_ ms $+ \Migration {name, up} -> insert name >> execSQL up+ where+ insert name = DB.execute conn "INSERT INTO migrations (name, ts) VALUES (?, ?);" . (name,) =<< getCurrentTime+ execSQL = SQLite3.exec $ DB.connectionHandle conn++initialize :: Connection -> IO ()+initialize conn =+ DB.execute_+ conn+ [sql|+ CREATE TABLE IF NOT EXISTS migrations (+ name TEXT NOT NULL,+ ts TEXT NOT NULL,+ PRIMARY KEY (name)+ );+ |]++migrationsToRun :: [Migration] -> [String] -> Either String [Migration]+migrationsToRun appMs [] = Right appMs+migrationsToRun [] dbMs = Left $ "database version is newer than the app: " <> intercalate ", " dbMs+migrationsToRun (a : as) (d : ds)+ | name a == d = migrationsToRun as ds+ | otherwise = Left $ "different migration in the app/database: " <> name a <> " / " <> d
− src/Simplex/Messaging/Agent/Store/SQLite/Schema.hs
@@ -1,167 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}--module Simplex.Messaging.Agent.Store.SQLite.Schema (createSchema) where--import Database.SQLite.Simple (Connection, Query, execute_)-import Database.SQLite.Simple.QQ (sql)--createSchema :: Connection -> IO ()-createSchema conn =- mapM_- (execute_ conn)- [ servers,- rcvQueues,- sndQueues,- connections,- messages,- rcvMessages,- sndMessages- ]---- port is either a port number or a service name, see Network.Socket.Info.ServiceName-servers :: Query-servers =- [sql|- CREATE TABLE IF NOT EXISTS servers(- host TEXT NOT NULL,- port TEXT NOT NULL,- key_hash BLOB,- PRIMARY KEY (host, port)- ) WITHOUT ROWID;- |]--rcvQueues :: Query-rcvQueues =- [sql|- CREATE TABLE IF NOT EXISTS rcv_queues(- host TEXT NOT NULL,- port TEXT NOT NULL,- rcv_id BLOB NOT NULL,- conn_alias BLOB NOT NULL,- rcv_private_key BLOB NOT NULL,- snd_id BLOB,- snd_key BLOB,- decrypt_key BLOB NOT NULL,- verify_key BLOB,- status TEXT NOT NULL,- PRIMARY KEY (host, port, rcv_id),- FOREIGN KEY (host, port) REFERENCES servers (host, port),- FOREIGN KEY (conn_alias)- REFERENCES connections (conn_alias)- ON DELETE CASCADE- DEFERRABLE INITIALLY DEFERRED,- UNIQUE (host, port, snd_id)- ) WITHOUT ROWID;- |]--sndQueues :: Query-sndQueues =- [sql|- CREATE TABLE IF NOT EXISTS snd_queues(- host TEXT NOT NULL,- port TEXT NOT NULL,- snd_id BLOB NOT NULL,- conn_alias BLOB NOT NULL,- snd_private_key BLOB NOT NULL,- encrypt_key BLOB NOT NULL,- sign_key BLOB NOT NULL,- status TEXT NOT NULL,- PRIMARY KEY (host, port, snd_id),- FOREIGN KEY (host, port) REFERENCES servers (host, port),- FOREIGN KEY (conn_alias)- REFERENCES connections (conn_alias)- ON DELETE CASCADE- DEFERRABLE INITIALLY DEFERRED- ) WITHOUT ROWID;- |]--connections :: Query-connections =- [sql|- CREATE TABLE IF NOT EXISTS connections(- conn_alias BLOB NOT NULL,- rcv_host TEXT,- rcv_port TEXT,- rcv_id BLOB,- snd_host TEXT,- snd_port TEXT,- snd_id BLOB,- last_internal_msg_id INTEGER NOT NULL,- last_internal_rcv_msg_id INTEGER NOT NULL,- last_internal_snd_msg_id INTEGER NOT NULL,- last_external_snd_msg_id INTEGER NOT NULL,- last_rcv_msg_hash BLOB NOT NULL,- last_snd_msg_hash BLOB NOT NULL,- PRIMARY KEY (conn_alias),- FOREIGN KEY (rcv_host, rcv_port, rcv_id) REFERENCES rcv_queues (host, port, rcv_id),- FOREIGN KEY (snd_host, snd_port, snd_id) REFERENCES snd_queues (host, port, snd_id)- ) WITHOUT ROWID;- |]--messages :: Query-messages =- [sql|- CREATE TABLE IF NOT EXISTS messages(- conn_alias BLOB NOT NULL,- internal_id INTEGER NOT NULL,- internal_ts TEXT NOT NULL,- internal_rcv_id INTEGER,- internal_snd_id INTEGER,- body TEXT NOT NULL,- PRIMARY KEY (conn_alias, internal_id),- FOREIGN KEY (conn_alias)- REFERENCES connections (conn_alias)- ON DELETE CASCADE,- FOREIGN KEY (conn_alias, internal_rcv_id)- REFERENCES rcv_messages (conn_alias, internal_rcv_id)- ON DELETE CASCADE- DEFERRABLE INITIALLY DEFERRED,- FOREIGN KEY (conn_alias, internal_snd_id)- REFERENCES snd_messages (conn_alias, internal_snd_id)- ON DELETE CASCADE- DEFERRABLE INITIALLY DEFERRED- ) WITHOUT ROWID;- |]--rcvMessages :: Query-rcvMessages =- [sql|- CREATE TABLE IF NOT EXISTS rcv_messages(- conn_alias BLOB NOT NULL,- internal_rcv_id INTEGER NOT NULL,- internal_id INTEGER NOT NULL,- external_snd_id INTEGER NOT NULL,- external_snd_ts TEXT NOT NULL,- broker_id BLOB NOT NULL,- broker_ts TEXT NOT NULL,- rcv_status TEXT NOT NULL,- ack_brocker_ts TEXT,- ack_sender_ts TEXT,- internal_hash BLOB NOT NULL,- external_prev_snd_hash BLOB NOT NULL,- integrity BLOB NOT NULL,- PRIMARY KEY (conn_alias, internal_rcv_id),- FOREIGN KEY (conn_alias, internal_id)- REFERENCES messages (conn_alias, internal_id)- ON DELETE CASCADE- ) WITHOUT ROWID;- |]--sndMessages :: Query-sndMessages =- [sql|- CREATE TABLE IF NOT EXISTS snd_messages(- conn_alias BLOB NOT NULL,- internal_snd_id INTEGER NOT NULL,- internal_id INTEGER NOT NULL,- snd_status TEXT NOT NULL,- sent_ts TEXT,- delivered_ts TEXT,- internal_hash BLOB NOT NULL,- PRIMARY KEY (conn_alias, internal_snd_id),- FOREIGN KEY (conn_alias, internal_id)- REFERENCES messages (conn_alias, internal_id)- ON DELETE CASCADE- ) WITHOUT ROWID;- |]
src/Simplex/Messaging/Client.hs view
@@ -53,9 +53,10 @@ 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.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. --@@ -98,6 +99,9 @@ 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@@ -111,6 +115,7 @@ defaultTransport = ("5223", transport @TCP), tcpTimeout = 4_000_000, smpPing = 30_000_000,+ smpBlockSize = Just 8192, smpCommandSize = 256 } @@ -127,7 +132,7 @@ -- 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@@ -172,7 +177,7 @@ 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@@ -195,22 +200,27 @@ 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 ()
src/Simplex/Messaging/Crypto.hs view
@@ -20,18 +20,20 @@ -- <https://hackage.haskell.org/package/cryptonite cryptonite package>. 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 @@ -- | 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,29 +137,53 @@ -- 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)+ fromString = parseString $ decode >=> decodePrivKey instance IsString PublicKey where- fromString = parseString (decode >=> decodePubKey)+ fromString = parseString $ decode >=> decodePubKey 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 @@ 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@@ -227,6 +256,7 @@ validKeySize = \case 128 -> True 256 -> True+ 384 -> True 512 -> True _ -> False
src/Simplex/Messaging/Parsers.hs view
@@ -30,7 +30,7 @@ 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,13 +42,16 @@ 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
src/Simplex/Messaging/Protocol.hs view
@@ -192,6 +192,8 @@ 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
src/Simplex/Messaging/Server.hs view
@@ -90,7 +90,8 @@ 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 () @@ -157,6 +158,7 @@ cryptoVerify $ case sigLen of 128 -> dummyKey128 256 -> dummyKey256+ 384 -> dummyKey384 512 -> dummyKey512 _ -> dummyKey256 sigLen = B.length $ C.unSignature sig@@ -169,6 +171,9 @@ 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==" @@ -292,16 +297,19 @@ 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)
src/Simplex/Messaging/Server/Env/STM.hs view
@@ -25,9 +25,11 @@ data ServerConfig = ServerConfig { transports :: [(ServiceName, ATransport)], tbqSize :: Natural,+ msgQueueQuota :: Natural, queueIdBytes :: Int, msgIdBytes :: Int, storeLog :: Maybe (StoreLog 'ReadMode),+ blockSize :: Int, serverPrivateKey :: C.FullPrivateKey -- serverId :: ByteString }@@ -86,7 +88,7 @@ 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)
src/Simplex/Messaging/Server/MsgStore.hs view
@@ -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 @@ } 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
src/Simplex/Messaging/Server/MsgStore/STM.hs view
@@ -8,11 +8,12 @@ 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 = 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 @@ 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
src/Simplex/Messaging/Transport.hs view
@@ -63,8 +63,10 @@ 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.String import Data.Word (Word32) import GHC.Generics (Generic) import GHC.IO.Exception (IOErrorType (..))@@ -73,7 +75,7 @@ import Network.Socket import Network.Transport.Internal (decodeNum16, decodeNum32, encodeEnum16, encodeEnum32, encodeWord32) import qualified Simplex.Messaging.Crypto as C-import Simplex.Messaging.Parsers (parse, parseAll, parseRead1)+import Simplex.Messaging.Parsers (parse, parseAll, parseRead1, parseString) import Simplex.Messaging.Util (bshow, liftError) import System.IO import System.IO.Error@@ -212,11 +214,14 @@ data SMPVersion = SMPVersion Int Int Int Int deriving (Eq, Ord) +instance IsString SMPVersion where+ fromString = parseString $ parseAll smpVersionP+ major :: SMPVersion -> (Int, Int) major (SMPVersion a b _ _) = (a, b) currentSMPVersion :: SMPVersion-currentSMPVersion = SMPVersion 0 3 2 0+currentSMPVersion = "0.4.0.0" serializeSMPVersion :: SMPVersion -> ByteString serializeSMPVersion (SMPVersion a b c d) = B.intercalate "." [bshow a, bshow b, bshow c, bshow d]@@ -340,21 +345,21 @@ -- 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 +379,14 @@ -- 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 +394,7 @@ 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 +413,8 @@ 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 +425,18 @@ 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
src/Simplex/Messaging/Util.hs view
@@ -50,3 +50,6 @@ 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)
tests/AgentTests.hs view
@@ -2,44 +2,64 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE PostfixOperators #-} {-# 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 Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import SMPAgentClient+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 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 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 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)+(<#:) = tGet SAgent+ -- | send transmission `t` to handle `h` and get response (#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent)-h #: t = tPutRaw h t >> 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`@@ -58,11 +78,11 @@ -- | receive message to handle `h` and validate that it is the expected one (<#) :: Transport c => c -> ATransmission 'Agent -> Expectation-h <# (corrId, cAlias, cmd) = tGet SAgent h `shouldReturn` (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-h <#= p = tGet SAgent h >>= (`shouldSatisfy` p . correctTransmission)+h <#= p = (h <#:) >>= (`shouldSatisfy` p . correctTransmission) -- | test that nothing is delivered to handle `h` during 10ms (#:#) :: Transport c => c -> String -> Expectation@@ -74,50 +94,183 @@ _ -> 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 ("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' <> " 14\nbob's connInfo") #> ("11", "alice", 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)- alice #: ("2", "bob", "SEND :hello") =#> \case ("2", "bob", SENT 1) -> True; _ -> False- alice #: ("3", "bob", "SEND :how are you?") =#> \case ("3", "bob", SENT 2) -> True; _ -> False+ alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 1)+ alice <# ("", "bob", SENT 1) bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False+ bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK)+ alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 2)+ alice <# ("", "bob", SENT 2) bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False- bob #: ("14", "alice", "SEND 9\nhello too") =#> \case ("14", "alice", SENT 3) -> 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- bob #: ("15", "alice", "SEND 9\nmessage 1") =#> \case ("15", "alice", SENT 4) -> 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", 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" +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' <> " 14\nbob's connInfo")+ ("", bobConn', Right (REQ confId "bob's connInfo")) <- (alice <#:)+ bobConn' `shouldBe` bobConn+ 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)+ alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 1)+ alice <# ("", bobConn, SENT 1)+ bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False+ bob #: ("12", aliceConn, "ACK 1") #> ("12", aliceConn, OK)+ alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 2)+ alice <# ("", bobConn, SENT 2)+ 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))+ 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", "bob", Right (INV qInfo)) <- alice1 #: ("1", "bob", "NEW")- let qInfo' = serializeSmpQueueInfo qInfo- bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON)- bob #: ("12", "alice", "SEND 5\nhello") =#> \case ("12", "alice", SENT _) -> True; _ -> False- bob #: ("13", "alice", "SEND 11\nhello again") =#> \case ("13", "alice", SENT _) -> True; _ -> False- alice1 <# ("", "bob", CON)+ (alice1, "alice") `connect` (bob, "bob")+ bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 1)+ bob <# ("", "alice", SENT 1) alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False+ alice1 #: ("1", "bob", "ACK 1") #> ("1", "bob", OK)+ bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 2)+ bob <# ("", "alice", SENT 2) 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") =#> \case ("14", "alice", SENT _) -> True; _ -> False+ 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 ()-testSubscrNotification _ (server, _) client = do- client #: ("1", "conn1", "NEW") =#> \case ("1", "conn1", INV _) -> True; _ -> False+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 +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 #: ("11", "bob", "ACK 1") #> ("11", "bob", OK)+ 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+ alice #: ("12", "bob", "ACK 2") #> ("12", "bob", OK)++ 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 #: ("11", "alice", "ACK 1") #> ("11", "alice", OK)+ 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+ bob #: ("12", "alice", "ACK 2") #> ("12", "alice", OK)++ 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")+ let qInfo' = serializeSmpQueueInfo qInfo+ h2 #: ("c2", name1, "JOIN " <> qInfo' <> " 5\ninfo2") #> ("c2", name1, 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)++-- 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" @@ -126,9 +279,8 @@ 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", "", "NEW") >#>= \case ("211", "", "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", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX")@@ -138,7 +290,7 @@ -- 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", "", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "", "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")
+ tests/AgentTests/FunctionalAPITests.hs view
@@ -0,0 +1,163 @@+{-# 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, 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.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"+ ("", _, 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)+ 1 <- sendMessage alice bobId "hello"+ get alice ##> ("", bobId, SENT 1)+ 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))+ 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+ ("", _, 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)+ 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+ ("", _, 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)+ 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+ ("", _, 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)+ 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+ 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
tests/AgentTests/SQLiteTests.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}@@ -8,9 +9,11 @@ module AgentTests.SQLiteTests (storeTests) where import Control.Concurrent.Async (concurrently_)+import Control.Concurrent.STM 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)@@ -22,8 +25,9 @@ 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 System.Random import Test.Hspec import UnliftIO.Directory (removeFile) @@ -39,7 +43,7 @@ connect2 :: IO (SQLiteStore, SQLiteStore) connect2 = do s1 <- createStore- s2 <- connectSQLiteStore $ dbFilePath s1+ s2 <- connectSQLiteStore (dbFilePath s1) 4 pure (s1, s2) createStore :: IO SQLiteStore@@ -47,12 +51,15 @@ -- 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) 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@@ -73,11 +80,13 @@ 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,24 +113,29 @@ 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 =- 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@@ -129,18 +143,19 @@ 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+cData1 = ConnData {connId = "conn1"}+ 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, decryptKey = C.safePrivateKey (1, 2, 3), verifyKey = Nothing, status = New@@ -151,74 +166,104 @@ 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),+ signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), status = New } 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 +273,10 @@ 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 +286,11 @@ 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,15 +300,15 @@ 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),+ signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), status = New } upgradeRcvConnToDuplex store "conn1" anotherSndQueue@@ -273,15 +320,14 @@ 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, decryptKey = C.safePrivateKey (1, 2, 3), verifyKey = Nothing, status = New@@ -295,40 +341,43 @@ 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 =@@ -351,31 +400,36 @@ 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 -> RcvQueue -> RcvMsgData -> Expectation-testCreateRcvMsg' store expectedPrevSndId expectedPrevHash rcvQueue rcvMsgData@RcvMsgData {..} = do- updateRcvIds store rcvQueue- `returnsResult` (internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)- createRcvMsg store rcvQueue rcvMsgData+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 internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)+ 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 =@@ -384,32 +438,37 @@ internalSndId, internalTs = ts, msgBody = hw,- internalHash+ internalHash,+ previousMsgHash = 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"
tests/SMPAgentClient.hs view
@@ -24,6 +24,7 @@ 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@@ -95,11 +96,15 @@ 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"@@ -139,13 +144,20 @@ _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 =- AgentConfig+ defaultAgentConfig { tcpPort = agentTestPort, smpServers = L.fromList ["localhost:5000#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="],- rsaKeySize = 2048 `div` 8,- connIdBytes = 12, tbqSize = 1, dbFile = testDB, smpCfg =@@ -153,16 +165,20 @@ { 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-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 @@ -173,7 +189,7 @@ testSMPAgentClientOn port' client = do runTransportClient agentTestHost port' $ \h -> do line <- liftIO $ getLn h- if line == "Welcome to SMP v0.3.2 agent"+ if line == "Welcome to SMP v0.4.0 agent" then client h else error $ "wrong welcome message: " <> B.unpack line
tests/SMPClient.hs view
@@ -39,6 +39,9 @@ testKeyHashStr :: B.ByteString testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" +testBlockSize :: Maybe Int+testBlockSize = Just 8192+ testKeyHash :: Maybe C.KeyHash testKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" @@ -48,7 +51,7 @@ 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 @@ -57,9 +60,11 @@ ServerConfig { transports = undefined, tbqSize = 1,+ msgQueueQuota = 4, queueIdBytes = 12, msgIdBytes = 6, storeLog = Nothing,+ blockSize = 8192, serverPrivateKey = -- full RSA private key (only for tests) "MIIFIwIBAAKCAQEArZyrri/NAwt5buvYjwu+B/MQeJUszDBpRgVqNddlI9kNwDXu\
tests/ServerTests.hs view
@@ -337,12 +337,19 @@ (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
tests/Test.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE TypeApplications #-} -import AgentTests+import AgentTests (agentTests) import ProtocolErrorTests import ServerTests import Simplex.Messaging.Transport (TCP, Transport (..))