mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 18:35:59 +00:00
2869 lines
131 KiB
Haskell
2869 lines
131 KiB
Haskell
{-# LANGUAGE AllowAmbiguousTypes #-}
|
|
{-# LANGUAGE BangPatterns #-}
|
|
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE ConstraintKinds #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE FunctionalDependencies #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedLists #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StrictData #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilyDependencies #-}
|
|
|
|
module Simplex.Messaging.Agent.Client
|
|
( AgentClient (..),
|
|
ProtocolTestFailure (..),
|
|
ProtocolTestStep (..),
|
|
ClntQueueReqData (..),
|
|
CQRData (..),
|
|
newAgentClient,
|
|
withConnLock,
|
|
withConnLocks,
|
|
withInvLock,
|
|
withLockMap,
|
|
withLocksMap,
|
|
getMapLock,
|
|
ipAddressProtected,
|
|
closeAgentClient,
|
|
closeProtocolServerClients,
|
|
reconnectServerClients,
|
|
reconnectSMPServer,
|
|
closeXFTPServerClient,
|
|
runSMPServerTest,
|
|
runXFTPServerTest,
|
|
runNTFServerTest,
|
|
getXFTPWorkPath,
|
|
newRcvQueue,
|
|
newRcvQueue_,
|
|
subscribeQueues,
|
|
subscribeUserServerQueues,
|
|
subscribeClientService,
|
|
processRcvServiceAssocs,
|
|
processClientNotices,
|
|
getQueueMessage,
|
|
decryptSMPMessage,
|
|
failSubscription,
|
|
addNewQueueSubscription,
|
|
getSubscriptions,
|
|
sendConfirmation,
|
|
sendInvitation,
|
|
temporaryAgentError,
|
|
temporaryOrHostError,
|
|
serverHostError,
|
|
secureQueue,
|
|
secureSndQueue,
|
|
addQueueLink,
|
|
deleteQueueLink,
|
|
secureGetQueueLink,
|
|
getQueueLink,
|
|
enableQueueNotifications,
|
|
EnableQueueNtfReq (..),
|
|
enableQueuesNtfs,
|
|
disableQueueNotifications,
|
|
DisableQueueNtfReq,
|
|
disableQueuesNtfs,
|
|
sendAgentMessage,
|
|
getQueueInfo,
|
|
agentNtfRegisterToken,
|
|
agentNtfVerifyToken,
|
|
agentNtfCheckToken,
|
|
agentNtfReplaceToken,
|
|
agentNtfDeleteToken,
|
|
agentNtfSetCronInterval,
|
|
agentNtfCreateSubscription,
|
|
agentNtfCreateSubscriptions,
|
|
agentNtfCheckSubscription,
|
|
agentNtfCheckSubscriptions,
|
|
agentNtfDeleteSubscription,
|
|
agentXFTPDownloadChunk,
|
|
agentXFTPNewChunk,
|
|
agentXFTPUploadChunk,
|
|
agentXFTPAddRecipients,
|
|
agentXFTPDeleteChunk,
|
|
agentCbDecrypt,
|
|
cryptoError,
|
|
sendAck,
|
|
suspendQueue,
|
|
deleteQueue,
|
|
deleteQueues,
|
|
logServer,
|
|
logSecret,
|
|
logSecret',
|
|
removeSubscription,
|
|
removeSubscriptions,
|
|
hasActiveSubscription,
|
|
hasPendingSubscription,
|
|
hasRemovedSubscription,
|
|
hasGetLock,
|
|
releaseGetLock,
|
|
activeClientSession,
|
|
agentClientStore,
|
|
agentDRG,
|
|
ServerQueueInfo (..),
|
|
AgentServersSummary (..),
|
|
ServerSessions (..),
|
|
SMPServerSubs (..),
|
|
getAgentSubsTotal,
|
|
getAgentServersSummary,
|
|
getAgentSubscriptions,
|
|
slowNetworkConfig,
|
|
protocolClientError,
|
|
clientServiceError,
|
|
Worker (..),
|
|
SessionVar (..),
|
|
SubscriptionsInfo (..),
|
|
SubInfo (..),
|
|
AgentOperation (..),
|
|
AgentOpState (..),
|
|
AgentState (..),
|
|
AgentLocks (..),
|
|
getAgentWorker,
|
|
getAgentWorker',
|
|
cancelWorker,
|
|
waitForWork,
|
|
hasWorkToDo,
|
|
hasWorkToDo',
|
|
withWork,
|
|
withWork_,
|
|
withWorkItems,
|
|
agentOperations,
|
|
agentOperationBracket,
|
|
waitUntilActive,
|
|
UserNetworkInfo (..),
|
|
UserNetworkType (..),
|
|
getFastNetworkConfig,
|
|
waitForUserNetwork,
|
|
isNetworkOnline,
|
|
isOnline,
|
|
throwWhenInactive,
|
|
throwWhenNoDelivery,
|
|
beginAgentOperation,
|
|
endAgentOperation,
|
|
waitUntilForeground,
|
|
waitWhileSuspended,
|
|
suspendSendingAndDatabase,
|
|
suspendOperation,
|
|
notifySuspended,
|
|
whenSuspending,
|
|
withStore,
|
|
withStore',
|
|
withStoreBatch,
|
|
withStoreBatch',
|
|
unsafeWithStore,
|
|
storeError,
|
|
notifySub,
|
|
notifySub',
|
|
userServers,
|
|
pickServer,
|
|
getNextServer,
|
|
withNextSrv,
|
|
incSMPServerStat,
|
|
incSMPServerStat',
|
|
incXFTPServerStat,
|
|
incXFTPServerStat',
|
|
incXFTPServerSizeStat,
|
|
incNtfServerStat,
|
|
incNtfServerStat',
|
|
AgentWorkersDetails (..),
|
|
getAgentWorkersDetails,
|
|
AgentWorkersSummary (..),
|
|
getAgentWorkersSummary,
|
|
AgentQueuesInfo (..),
|
|
getAgentQueuesInfo,
|
|
SMPTransportSession,
|
|
NtfTransportSession,
|
|
XFTPTransportSession,
|
|
ProxiedRelay (..),
|
|
SMPConnectedClient (..),
|
|
)
|
|
where
|
|
|
|
import Control.Applicative ((<|>))
|
|
import Control.Concurrent (ThreadId, killThread)
|
|
import Control.Concurrent.Async (Async, uninterruptibleCancel)
|
|
import Control.Concurrent.STM (retry)
|
|
import Control.Exception (AsyncException (..), BlockedIndefinitelyOnSTM (..))
|
|
import Control.Logger.Simple
|
|
import Control.Monad
|
|
import Control.Monad.Except
|
|
import Control.Monad.IO.Unlift
|
|
import Control.Monad.Reader
|
|
import Control.Monad.Trans.Except
|
|
import Crypto.Random (ChaChaDRG)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.TH as J
|
|
import Data.Bifunctor (bimap, first, second)
|
|
import qualified Data.ByteString.Base64 as B64
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.Composition ((.:), (.:.))
|
|
import Data.Containers.ListUtils (nubOrd)
|
|
import Data.Either (isRight, partitionEithers)
|
|
import Data.Functor (($>))
|
|
import Data.Int (Int64)
|
|
import Data.List (find, foldl')
|
|
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 (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)
|
|
import Data.Set (Set)
|
|
import qualified Data.Set as S
|
|
import Data.Text (Text)
|
|
import Data.Text.Encoding
|
|
import Data.Time (UTCTime, addUTCTime, defaultTimeLocale, formatTime, getCurrentTime)
|
|
import Data.Time.Clock.System (getSystemTime)
|
|
import Data.Word (Word16)
|
|
import qualified Data.X509.Validation as XV
|
|
import Network.Socket (HostName)
|
|
import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError)
|
|
import qualified Simplex.FileTransfer.Client as X
|
|
import Simplex.FileTransfer.Description (ChunkReplicaId (..), FileDigest (..), kb)
|
|
import Simplex.FileTransfer.Protocol (FileInfo (..), FileResponse)
|
|
import Simplex.FileTransfer.Transport (XFTPErrorType (DIGEST), XFTPRcvChunkSpec (..), XFTPVersion)
|
|
import qualified Simplex.FileTransfer.Transport as XFTP
|
|
import Simplex.FileTransfer.Types (DeletedSndChunkReplica (..), NewSndChunkReplica (..), RcvFileChunkReplica (..), SndFileChunk (..), SndFileChunkReplica (..))
|
|
import Simplex.FileTransfer.Util (uniqueCombine)
|
|
import Simplex.Messaging.Agent.Env.SQLite
|
|
import Simplex.Messaging.Agent.Lock
|
|
import Simplex.Messaging.Agent.Protocol
|
|
import Simplex.Messaging.Agent.RetryInterval
|
|
import Simplex.Messaging.Agent.Stats
|
|
import Simplex.Messaging.Agent.Store
|
|
import Simplex.Messaging.Agent.Store.AgentStore
|
|
import Simplex.Messaging.Agent.Store.Common (DBStore)
|
|
import Simplex.Messaging.Agent.Store.DB (SQLError)
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import Simplex.Messaging.Agent.Store.Entity
|
|
import Simplex.Messaging.Agent.TSessionSubs (TSessionSubs)
|
|
import qualified Simplex.Messaging.Agent.TSessionSubs as SS
|
|
import Simplex.Messaging.Client
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Notifications.Client
|
|
import Simplex.Messaging.Notifications.Protocol
|
|
import Simplex.Messaging.Notifications.Transport (NTFVersion)
|
|
import Simplex.Messaging.Notifications.Types
|
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse, sumTypeJSON)
|
|
import Simplex.Messaging.Protocol
|
|
( AProtocolType (..),
|
|
BrokerMsg,
|
|
EntityId (..),
|
|
ServiceId,
|
|
ErrorType,
|
|
NetworkError (..),
|
|
MsgFlags (..),
|
|
MsgId,
|
|
NtfServer,
|
|
NtfServerWithAuth,
|
|
ProtoServer,
|
|
ProtoServerWithAuth (..),
|
|
Protocol (..),
|
|
ProtocolServer (..),
|
|
ProtocolType (..),
|
|
ProtocolTypeI (..),
|
|
QueueIdsKeys (..),
|
|
ServerNtfCreds (..),
|
|
RcvMessage (..),
|
|
RcvNtfPublicDhKey,
|
|
SMPMsgMeta (..),
|
|
SProtocolType (..),
|
|
ServiceSub (..),
|
|
ServiceSubResult (..),
|
|
ServiceSubError (..),
|
|
SndPublicAuthKey,
|
|
SubscriptionMode (..),
|
|
NewNtfCreds (..),
|
|
QueueReqData (..),
|
|
QueueLinkData,
|
|
UserProtocol,
|
|
VersionRangeSMPC,
|
|
VersionSMPC,
|
|
XFTPServer,
|
|
XFTPServerWithAuth,
|
|
pattern NoEntity,
|
|
senderCanSecure,
|
|
serviceSubResult,
|
|
)
|
|
import qualified Simplex.Messaging.Protocol as SMP
|
|
import Simplex.Messaging.Protocol.Types
|
|
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
|
import Simplex.Messaging.Session
|
|
import Simplex.Messaging.SystemTime
|
|
import Simplex.Messaging.TMap (TMap)
|
|
import qualified Simplex.Messaging.TMap as TM
|
|
import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion), TransportError (..), TransportPeer (..), sndAuthKeySMPVersion, shortLinksSMPVersion, newNtfCredsSMPVersion)
|
|
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
|
import Simplex.Messaging.Transport.Credentials
|
|
import Simplex.Messaging.Util
|
|
import Simplex.Messaging.Version
|
|
import System.Mem.Weak (Weak, deRefWeak)
|
|
import System.Random (randomR)
|
|
import UnliftIO (mapConcurrently, timeout)
|
|
import UnliftIO.Async (async)
|
|
import UnliftIO.Concurrent (forkIO, mkWeakThreadId)
|
|
import UnliftIO.Directory (doesFileExist, getTemporaryDirectory, removeFile)
|
|
import qualified UnliftIO.Exception as E
|
|
import UnliftIO.STM
|
|
#if !defined(dbPostgres)
|
|
import qualified Database.SQLite.Simple as SQL
|
|
#endif
|
|
|
|
type ClientVar msg = SessionVar (Either (AgentErrorType, Maybe UTCTime) (Client msg))
|
|
|
|
type SMPClientVar = ClientVar SMP.BrokerMsg
|
|
|
|
type NtfClientVar = ClientVar NtfResponse
|
|
|
|
type XFTPClientVar = ClientVar FileResponse
|
|
|
|
type NtfTransportSession = TransportSession NtfResponse
|
|
|
|
type XFTPTransportSession = TransportSession FileResponse
|
|
|
|
data AgentClient = AgentClient
|
|
{ acThread :: TVar (Maybe (Weak ThreadId)),
|
|
active :: TVar Bool,
|
|
subQ :: TBQueue ATransmission,
|
|
msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg),
|
|
smpServers :: TMap UserId (UserServers 'PSMP),
|
|
smpClients :: TMap SMPTransportSession SMPClientVar,
|
|
useClientServices :: TMap UserId Bool,
|
|
-- smpProxiedRelays:
|
|
-- SMPTransportSession defines connection from proxy to relay,
|
|
-- SMPServerWithAuth defines client connected to SMP proxy (with the same userId and entityId in TransportSession)
|
|
smpProxiedRelays :: TMap SMPTransportSession SMPServerWithAuth,
|
|
ntfServers :: TVar [NtfServer],
|
|
ntfClients :: TMap NtfTransportSession NtfClientVar,
|
|
xftpServers :: TMap UserId (UserServers 'PXFTP),
|
|
xftpClients :: TMap XFTPTransportSession XFTPClientVar,
|
|
useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks
|
|
presetDomains :: [HostName],
|
|
presetServers :: [SMPServer],
|
|
userNetworkInfo :: TVar UserNetworkInfo,
|
|
userNetworkUpdated :: TVar (Maybe UTCTime),
|
|
subscrConns :: TVar (Set ConnId),
|
|
currentSubs :: TSessionSubs,
|
|
removedSubs :: TMap (UserId, SMPServer) (TMap SMP.RecipientId SMPClientError),
|
|
clientNotices :: TMap (Maybe SMPServer) (Maybe SystemSeconds),
|
|
clientNoticesLock :: TMVar (),
|
|
workerSeq :: TVar Int,
|
|
smpDeliveryWorkers :: TMap SndQAddr (Worker, TMVar ()),
|
|
asyncCmdWorkers :: TMap (ConnId, Maybe SMPServer) Worker,
|
|
ntfNetworkOp :: TVar AgentOpState,
|
|
rcvNetworkOp :: TVar AgentOpState,
|
|
msgDeliveryOp :: TVar AgentOpState,
|
|
sndNetworkOp :: TVar AgentOpState,
|
|
databaseOp :: TVar AgentOpState,
|
|
agentState :: TVar AgentState,
|
|
getMsgLocks :: TMap (SMPServer, SMP.RecipientId) (TMVar ()),
|
|
-- locks to prevent concurrent operations with connection
|
|
connLocks :: TMap ConnId Lock,
|
|
-- locks to prevent concurrent operations with connection request invitations
|
|
invLocks :: TMap ByteString Lock,
|
|
-- lock to prevent concurrency between periodic and async connection deletions
|
|
deleteLock :: Lock,
|
|
-- smpSubWorkers for SMP servers sessions
|
|
smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())),
|
|
clientId :: Int,
|
|
agentEnv :: Env,
|
|
proxySessTs :: TVar UTCTime,
|
|
smpServersStats :: TMap (UserId, SMPServer) AgentSMPServerStats,
|
|
xftpServersStats :: TMap (UserId, XFTPServer) AgentXFTPServerStats,
|
|
ntfServersStats :: TMap (UserId, NtfServer) AgentNtfServerStats,
|
|
srvStatsStartedAt :: TVar UTCTime
|
|
}
|
|
|
|
data SMPConnectedClient = SMPConnectedClient
|
|
{ connectedClient :: SMPClient,
|
|
proxiedRelays :: TMap SMPServer ProxiedRelayVar
|
|
}
|
|
|
|
type ProxiedRelayVar = SessionVar (Either AgentErrorType ProxiedRelay)
|
|
|
|
getAgentWorker :: (Ord k, Show k, AnyError e, MonadUnliftIO m) => String -> Bool -> AgentClient -> k -> TMap k Worker -> (Worker -> ExceptT e m ()) -> m Worker
|
|
getAgentWorker = getAgentWorker' id pure
|
|
{-# INLINE getAgentWorker #-}
|
|
|
|
getAgentWorker' :: forall a k e m. (Ord k, Show k, AnyError e, MonadUnliftIO m) => (a -> Worker) -> (Worker -> STM a) -> String -> Bool -> AgentClient -> k -> TMap k a -> (a -> ExceptT e m ()) -> m a
|
|
getAgentWorker' toW fromW name hasWork c@AgentClient {agentEnv} key ws work = do
|
|
atomically (getWorker >>= maybe createWorker whenExists) >>= \w -> runWorker w $> w
|
|
where
|
|
getWorker = TM.lookup key ws
|
|
createWorker = do
|
|
w <- fromW =<< newWorker c
|
|
TM.insert key w ws
|
|
pure w
|
|
whenExists w
|
|
| hasWork = hasWorkToDo (toW w) $> w
|
|
| otherwise = pure w
|
|
runWorker w = runWorkerAsync (toW w) runWork
|
|
where
|
|
runWork :: m ()
|
|
runWork = tryAllErrors' (work w) >>= restartOrDelete
|
|
restartOrDelete :: Either e () -> m ()
|
|
restartOrDelete e_ = do
|
|
t <- liftIO getSystemTime
|
|
let maxRestarts = maxWorkerRestartsPerMin $ config agentEnv
|
|
-- worker may terminate because it was deleted from the map (getWorker returns Nothing), then it won't restart
|
|
restart <- atomically $ getWorker >>= maybe (pure False) (shouldRestart e_ (toW w) t maxRestarts)
|
|
when restart runWork
|
|
shouldRestart e_ Worker {workerId = wId, doWork, action, restarts} t maxRestarts w'
|
|
| wId == workerId (toW w') = do
|
|
rc <- readTVar restarts
|
|
isActive <- readTVar $ active c
|
|
checkRestarts isActive $ updateRestartCount t rc
|
|
| otherwise =
|
|
pure False -- there is a new worker in the map, no action
|
|
where
|
|
checkRestarts isActive rc
|
|
| isActive && restartCount rc < maxRestarts = do
|
|
writeTVar restarts rc
|
|
hasWorkToDo' doWork
|
|
void $ tryPutTMVar action Nothing
|
|
notifyErr INTERNAL
|
|
pure True
|
|
| otherwise = do
|
|
TM.delete key ws
|
|
when isActive $ notifyErr $ CRITICAL True
|
|
pure False
|
|
where
|
|
notifyErr err = do
|
|
let e = either ((", error: " <>) . show) (\_ -> ", no error") e_
|
|
msg = "Worker " <> name <> " for " <> show key <> " terminated " <> show (restartCount rc) <> " times" <> e
|
|
writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err msg)
|
|
|
|
newWorker :: AgentClient -> STM Worker
|
|
newWorker c = do
|
|
workerId <- stateTVar (workerSeq c) $ \next -> (next, next + 1)
|
|
doWork <- newTMVar () -- new worker is created with "some work to do" (indicated by () in TMVar)
|
|
action <- newTMVar Nothing
|
|
restarts <- newTVar $ RestartCount 0 0
|
|
pure Worker {workerId, doWork, action, restarts}
|
|
|
|
runWorkerAsync :: MonadUnliftIO m => Worker -> m () -> m ()
|
|
runWorkerAsync Worker {action} work =
|
|
E.bracket
|
|
(atomically $ takeTMVar action) -- get current action, locking to avoid race conditions
|
|
(atomically . tryPutTMVar action) -- if it was running (or if start crashes), put it back and unlock (don't lock if it was just started)
|
|
(\a -> when (isNothing a) start) -- start worker if it's not running
|
|
where
|
|
start = atomically . putTMVar action . Just =<< mkWeakThreadId =<< forkIO work
|
|
|
|
data AgentOperation = AONtfNetwork | AORcvNetwork | AOMsgDelivery | AOSndNetwork | AODatabase
|
|
deriving (Eq, Show)
|
|
|
|
agentOpSel :: AgentOperation -> (AgentClient -> TVar AgentOpState)
|
|
agentOpSel = \case
|
|
AONtfNetwork -> ntfNetworkOp
|
|
AORcvNetwork -> rcvNetworkOp
|
|
AOMsgDelivery -> msgDeliveryOp
|
|
AOSndNetwork -> sndNetworkOp
|
|
AODatabase -> databaseOp
|
|
|
|
agentOperations :: [AgentClient -> TVar AgentOpState]
|
|
agentOperations = [ntfNetworkOp, rcvNetworkOp, msgDeliveryOp, sndNetworkOp, databaseOp]
|
|
|
|
data AgentOpState = AgentOpState {opSuspended :: !Bool, opsInProgress :: !Int}
|
|
|
|
data AgentState = ASForeground | ASSuspending | ASSuspended
|
|
deriving (Eq, Show)
|
|
|
|
data AgentLocks = AgentLocks
|
|
{ connLocks :: Map Text Text,
|
|
invLocks :: Map Text Text,
|
|
delLock :: Maybe Text
|
|
}
|
|
deriving (Show)
|
|
|
|
data UserNetworkInfo = UserNetworkInfo
|
|
{ networkType :: UserNetworkType,
|
|
online :: Bool
|
|
}
|
|
deriving (Show)
|
|
|
|
isNetworkOnline :: AgentClient -> STM Bool
|
|
isNetworkOnline c = isOnline <$> readTVar (userNetworkInfo c)
|
|
|
|
isOnline :: UserNetworkInfo -> Bool
|
|
isOnline UserNetworkInfo {networkType, online} = networkType /= UNNone && online
|
|
|
|
data UserNetworkType = UNNone | UNCellular | UNWifi | UNEthernet | UNOther
|
|
deriving (Eq, Show)
|
|
|
|
-- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's.
|
|
newAgentClient :: Int -> InitialAgentServers -> UTCTime -> Map (Maybe SMPServer) (Maybe SystemSeconds) -> Env -> IO AgentClient
|
|
newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg, useServices, presetDomains, presetServers} currentTs notices agentEnv = do
|
|
let cfg = config agentEnv
|
|
qSize = tbqSize cfg
|
|
proxySessTs <- newTVarIO =<< getCurrentTime
|
|
acThread <- newTVarIO Nothing
|
|
active <- newTVarIO True
|
|
subQ <- newTBQueueIO qSize
|
|
msgQ <- newTBQueueIO qSize
|
|
smpServers <- newTVarIO $ M.map mkUserServers smp
|
|
smpClients <- TM.emptyIO
|
|
useClientServices <- newTVarIO useServices
|
|
smpProxiedRelays <- TM.emptyIO
|
|
ntfServers <- newTVarIO ntf
|
|
ntfClients <- TM.emptyIO
|
|
xftpServers <- newTVarIO $ M.map mkUserServers xftp
|
|
xftpClients <- TM.emptyIO
|
|
useNetworkConfig <- newTVarIO (slowNetworkConfig netCfg, netCfg)
|
|
userNetworkInfo <- newTVarIO $ UserNetworkInfo UNOther True
|
|
userNetworkUpdated <- newTVarIO Nothing
|
|
subscrConns <- newTVarIO S.empty
|
|
currentSubs <- SS.emptyIO
|
|
removedSubs <- TM.emptyIO
|
|
clientNotices <- newTVarIO notices
|
|
clientNoticesLock <- newTMVarIO ()
|
|
workerSeq <- newTVarIO 0
|
|
smpDeliveryWorkers <- TM.emptyIO
|
|
asyncCmdWorkers <- TM.emptyIO
|
|
ntfNetworkOp <- newTVarIO $ AgentOpState False 0
|
|
rcvNetworkOp <- newTVarIO $ AgentOpState False 0
|
|
msgDeliveryOp <- newTVarIO $ AgentOpState False 0
|
|
sndNetworkOp <- newTVarIO $ AgentOpState False 0
|
|
databaseOp <- newTVarIO $ AgentOpState False 0
|
|
agentState <- newTVarIO ASForeground
|
|
getMsgLocks <- TM.emptyIO
|
|
connLocks <- TM.emptyIO
|
|
invLocks <- TM.emptyIO
|
|
deleteLock <- createLockIO
|
|
smpSubWorkers <- TM.emptyIO
|
|
smpServersStats <- TM.emptyIO
|
|
xftpServersStats <- TM.emptyIO
|
|
ntfServersStats <- TM.emptyIO
|
|
srvStatsStartedAt <- newTVarIO currentTs
|
|
return
|
|
AgentClient
|
|
{ acThread,
|
|
active,
|
|
subQ,
|
|
msgQ,
|
|
smpServers,
|
|
smpClients,
|
|
useClientServices,
|
|
smpProxiedRelays,
|
|
ntfServers,
|
|
ntfClients,
|
|
xftpServers,
|
|
xftpClients,
|
|
useNetworkConfig,
|
|
presetDomains,
|
|
presetServers,
|
|
userNetworkInfo,
|
|
userNetworkUpdated,
|
|
subscrConns,
|
|
currentSubs,
|
|
removedSubs,
|
|
clientNotices,
|
|
clientNoticesLock,
|
|
workerSeq,
|
|
smpDeliveryWorkers,
|
|
asyncCmdWorkers,
|
|
ntfNetworkOp,
|
|
rcvNetworkOp,
|
|
msgDeliveryOp,
|
|
sndNetworkOp,
|
|
databaseOp,
|
|
agentState,
|
|
getMsgLocks,
|
|
connLocks,
|
|
invLocks,
|
|
deleteLock,
|
|
smpSubWorkers,
|
|
clientId,
|
|
agentEnv,
|
|
proxySessTs,
|
|
smpServersStats,
|
|
xftpServersStats,
|
|
ntfServersStats,
|
|
srvStatsStartedAt
|
|
}
|
|
|
|
slowNetworkConfig :: NetworkConfig -> NetworkConfig
|
|
slowNetworkConfig cfg@NetworkConfig {tcpConnectTimeout, tcpTimeout, tcpTimeoutPerKb} =
|
|
cfg {tcpConnectTimeout = slowTimeout tcpConnectTimeout, tcpTimeout = slowTimeout tcpTimeout, tcpTimeoutPerKb = slow tcpTimeoutPerKb}
|
|
where
|
|
slowTimeout (NetworkTimeout t1 t2) = NetworkTimeout (slow t1) (slow t2)
|
|
slow :: Integral a => a -> a
|
|
slow t = (t * 3) `div` 2
|
|
|
|
agentClientStore :: AgentClient -> DBStore
|
|
agentClientStore AgentClient {agentEnv = Env {store}} = store
|
|
{-# INLINE agentClientStore #-}
|
|
|
|
agentDRG :: AgentClient -> TVar ChaChaDRG
|
|
agentDRG AgentClient {agentEnv = Env {random}} = random
|
|
{-# INLINE agentDRG #-}
|
|
|
|
getServiceCredentials :: AgentClient -> UserId -> SMPServer -> AM (Maybe (ServiceCredentials, Maybe ServiceId))
|
|
getServiceCredentials c userId srv =
|
|
liftIO (TM.lookupIO userId $ useClientServices c)
|
|
$>>= \useService -> if useService then Just <$> getService else pure Nothing
|
|
where
|
|
getService :: AM (ServiceCredentials, Maybe ServiceId)
|
|
getService = do
|
|
let g = agentDRG c
|
|
((C.KeyHash kh, serviceCreds), serviceId_) <-
|
|
withStore' c $ \db ->
|
|
getClientServiceCredentials db userId srv >>= \case
|
|
Just service -> pure service
|
|
Nothing -> do
|
|
cred <- genCredentials g Nothing (25, 24 * 999999) "simplex"
|
|
let tlsCreds = tlsCredentials [cred]
|
|
createClientService db userId srv tlsCreds
|
|
pure (tlsCreds, Nothing)
|
|
serviceSignKey <- liftEitherWith INTERNAL $ C.x509ToPrivate' $ snd serviceCreds
|
|
let creds = ServiceCredentials {serviceRole = SRMessaging, serviceCreds, serviceCertHash = XV.Fingerprint kh, serviceSignKey}
|
|
pure (creds, serviceId_)
|
|
|
|
class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg -> err where
|
|
type Client msg = c | c -> msg
|
|
getProtocolServerClient :: AgentClient -> NetworkRequestMode -> TransportSession msg -> AM (Client msg)
|
|
type ProtoClient msg = c | c -> msg
|
|
protocolClient :: Client msg -> ProtoClient msg
|
|
clientProtocolError :: HostName -> err -> AgentErrorType
|
|
closeProtocolServerClient :: ProtoClient msg -> IO ()
|
|
clientServer :: ProtoClient msg -> String
|
|
clientTransportHost :: ProtoClient msg -> TransportHost
|
|
|
|
instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where
|
|
type Client BrokerMsg = SMPConnectedClient
|
|
getProtocolServerClient = getSMPServerClient
|
|
type ProtoClient BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg
|
|
protocolClient = connectedClient
|
|
clientProtocolError = SMP
|
|
closeProtocolServerClient = closeProtocolClient
|
|
clientServer = protocolClientServer
|
|
clientTransportHost = transportHost'
|
|
|
|
instance ProtocolServerClient NTFVersion ErrorType NtfResponse where
|
|
type Client NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse
|
|
getProtocolServerClient = getNtfServerClient
|
|
type ProtoClient NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse
|
|
protocolClient = id
|
|
clientProtocolError = NTF
|
|
closeProtocolServerClient = closeProtocolClient
|
|
clientServer = protocolClientServer
|
|
clientTransportHost = transportHost'
|
|
|
|
instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where
|
|
type Client FileResponse = XFTPClient
|
|
getProtocolServerClient c _ = getXFTPServerClient c
|
|
type ProtoClient FileResponse = XFTPClient
|
|
protocolClient = id
|
|
clientProtocolError = XFTP
|
|
closeProtocolServerClient = X.closeXFTPClient
|
|
clientServer = X.xftpClientServer
|
|
clientTransportHost = X.xftpTransportHost
|
|
|
|
getSMPServerClient :: AgentClient -> NetworkRequestMode -> SMPTransportSession -> AM SMPConnectedClient
|
|
getSMPServerClient c@AgentClient {active, smpClients, workerSeq} nm tSess = do
|
|
unlessM (readTVarIO active) $ throwE INACTIVE
|
|
ts <- liftIO getCurrentTime
|
|
atomically (getSessVar workerSeq tSess smpClients ts)
|
|
>>= either newClient (waitForProtocolClient c nm tSess smpClients)
|
|
where
|
|
newClient v = do
|
|
prs <- liftIO TM.emptyIO
|
|
smpConnectClient c nm tSess prs v
|
|
|
|
getSMPProxyClient :: AgentClient -> NetworkRequestMode -> Maybe SMPServerWithAuth -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay)
|
|
getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} nm proxySrv_ destSess@(userId, destSrv, qId) = do
|
|
unlessM (readTVarIO active) $ throwE INACTIVE
|
|
proxySrv <- maybe (getNextServer c userId proxySrvs [destSrv]) pure proxySrv_
|
|
ts <- liftIO getCurrentTime
|
|
atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) ->
|
|
either (newProxyClient tSess auth ts) (waitForProxyClient tSess auth) v
|
|
where
|
|
getClientVar :: SMPServerWithAuth -> UTCTime -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar)
|
|
getClientVar proxySrv ts = do
|
|
ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure
|
|
let tSess = (userId, srv, qId)
|
|
(tSess,auth,) <$> getSessVar workerSeq tSess smpClients ts
|
|
newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> UTCTime -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay)
|
|
newProxyClient tSess auth ts v = do
|
|
prs <- liftIO TM.emptyIO
|
|
-- we do not need to check if it is a new proxied relay session,
|
|
-- as the client is just created and there are no sessions yet
|
|
rv <- atomically $ either id id <$> getSessVar workerSeq destSrv prs ts
|
|
clnt <- smpConnectClient c nm tSess prs v
|
|
(clnt,) <$> newProxiedRelay clnt auth rv
|
|
waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay)
|
|
waitForProxyClient tSess auth v = do
|
|
clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c nm tSess smpClients v
|
|
ts <- liftIO getCurrentTime
|
|
sess <-
|
|
atomically (getSessVar workerSeq destSrv prs ts)
|
|
>>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess)
|
|
pure (clnt, sess)
|
|
newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay)
|
|
newProxiedRelay (SMPConnectedClient smp prs) proxyAuth rv =
|
|
tryAllErrors (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp nm destSrv proxyAuth) >>= \case
|
|
Right sess -> do
|
|
atomically $ putTMVar (sessionVar rv) (Right sess)
|
|
pure $ Right sess
|
|
Left e -> do
|
|
atomically $ do
|
|
unless (serverHostError e) $ do
|
|
removeSessVar rv destSrv prs
|
|
TM.delete destSess smpProxiedRelays
|
|
putTMVar (sessionVar rv) (Left e)
|
|
pure $ Left e
|
|
waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay)
|
|
waitForProxiedRelay (_, srv, _) rv = do
|
|
NetworkConfig {tcpConnectTimeout} <- getNetworkConfig c
|
|
sess_ <- liftIO $ netTimeoutInt tcpConnectTimeout nm `timeout` atomically (readTMVar $ sessionVar rv)
|
|
pure $ case sess_ of
|
|
Just (Right sess) -> Right sess
|
|
Just (Left e) -> Left e
|
|
Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT
|
|
|
|
smpConnectClient :: AgentClient -> NetworkRequestMode -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient
|
|
smpConnectClient c@AgentClient {smpClients, msgQ, proxySessTs, presetDomains} nm tSess@(userId, srv, _) prs v =
|
|
newProtocolClient c tSess smpClients connectClient v
|
|
`catchAllErrors` \e -> lift (resubscribeSMPSession c tSess) >> throwE e
|
|
where
|
|
connectClient :: SMPClientVar -> AM SMPConnectedClient
|
|
connectClient v' = do
|
|
cfg <- lift $ getClientConfig c smpCfg
|
|
g <- asks random
|
|
service <- getServiceCredentials c userId srv
|
|
let cfg' = cfg {serviceCredentials = fst <$> service}
|
|
env <- ask
|
|
smp <- liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do
|
|
ts <- readTVarIO proxySessTs
|
|
ExceptT $ getProtocolClient g nm tSess cfg' presetDomains (Just msgQ) ts $ smpClientDisconnected c tSess env v' prs
|
|
atomically $ SS.setSessionId tSess (sessionId $ thParams smp) $ currentSubs c
|
|
updateClientService service smp
|
|
pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs}
|
|
updateClientService service smp = case (service, smpClientServiceId smp) of
|
|
(Just (_, serviceId_), Just serviceId) -> withStore' c $ \db -> do
|
|
setClientServiceId db userId srv serviceId
|
|
forM_ serviceId_ $ \sId -> when (sId /= serviceId) $ removeRcvServiceAssocs db userId srv
|
|
(Just _, Nothing) -> withStore' c $ \db -> deleteClientService db userId srv -- e.g., server version downgrade
|
|
(Nothing, Just _) -> logError "server returned serviceId without service credentials in request"
|
|
(Nothing, Nothing) -> pure ()
|
|
|
|
smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO ()
|
|
smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess@(userId, srv, cId) env v prs client = do
|
|
removeClientAndSubs >>= serverDown
|
|
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
|
where
|
|
-- we make active subscriptions pending only if the client for tSess was current (in the map) and active,
|
|
-- because we can have a race condition when a new current client could have already
|
|
-- made subscriptions active, and the old client would be processing diconnection later.
|
|
removeClientAndSubs :: IO ([RcvQueueSub], [ConnId], Maybe ServiceSub)
|
|
removeClientAndSubs = atomically $ do
|
|
removeSessVar v tSess smpClients
|
|
ifM (readTVar active) removeSubs (pure ([], [], Nothing))
|
|
where
|
|
sessId = sessionId $ thParams client
|
|
removeSubs = do
|
|
mode <- getSessionMode c
|
|
(subs, serviceSub_) <- SS.setSubsPending mode tSess sessId $ currentSubs c
|
|
let qs = M.elems subs
|
|
cs = nubOrd $ map qConnId qs
|
|
-- this removes proxied relays that this client created sessions to
|
|
destSrvs <- M.keys <$> readTVar prs
|
|
forM_ destSrvs $ \destSrv -> TM.delete (userId, destSrv, cId) smpProxiedRelays
|
|
pure (qs, cs, serviceSub_)
|
|
|
|
serverDown :: ([RcvQueueSub], [ConnId], Maybe ServiceSub) -> IO ()
|
|
serverDown (qs, conns, serviceSub_) = whenM (readTVarIO active) $ do
|
|
notifySub c $ hostEvent' DISCONNECT client
|
|
unless (null conns) $ notifySub c $ DOWN srv conns
|
|
mapM_ (notifySub c . SERVICE_DOWN srv) serviceSub_
|
|
unless (null qs && isNothing serviceSub_) $ do
|
|
releaseGetLocksIO c qs
|
|
mode <- getSessionModeIO c
|
|
let resubscribe
|
|
| (mode == TSMEntity) == isJust cId = resubscribeSMPSession c tSess
|
|
| otherwise = do
|
|
mapM_ (runExceptT . resubscribeClientService c tSess) serviceSub_
|
|
unless (null qs) $ void $ subscribeQueues c True qs
|
|
runReaderT resubscribe env
|
|
|
|
resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' ()
|
|
resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = do
|
|
ts <- liftIO getCurrentTime
|
|
atomically (getWorkerVar ts) >>= mapM_ (either newSubWorker (\_ -> pure ()))
|
|
where
|
|
getWorkerVar ts =
|
|
ifM
|
|
(not <$> SS.hasPendingSubs tSess (currentSubs c))
|
|
(pure Nothing) -- prevent race with cleanup and adding pending queues in another call
|
|
(Just <$> getSessVar workerSeq tSess smpSubWorkers ts)
|
|
newSubWorker v = do
|
|
a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v)
|
|
atomically $ putTMVar (sessionVar v) a
|
|
runSubWorker = do
|
|
ri <- asks $ reconnectInterval . config
|
|
withRetryForeground ri isForeground (isNetworkOnline c) $ \_ loop -> do
|
|
(pendingSubs, pendingSS) <- atomically $ SS.getPendingSubs tSess $ currentSubs c
|
|
unless (M.null pendingSubs && isNothing pendingSS) $ do
|
|
liftIO $ waitUntilForeground c
|
|
liftIO $ waitForUserNetwork c
|
|
mapM_ (handleNotify . void . runExceptT . resubscribeClientService c tSess) pendingSS
|
|
unless (M.null pendingSubs) $ handleNotify $ resubscribeSessQueues c tSess $ M.elems pendingSubs
|
|
loop
|
|
isForeground = (ASForeground ==) <$> readTVar (agentState c)
|
|
cleanup :: SessionVar (Async ()) -> STM ()
|
|
cleanup v = do
|
|
-- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar.
|
|
-- Not waiting may result in terminated worker remaining in the map.
|
|
whenM (isEmptyTMVar $ sessionVar v) retry
|
|
removeSessVar v tSess smpSubWorkers
|
|
handleNotify :: AM' () -> AM' ()
|
|
handleNotify = E.handleAny $ notifySub' c "" . ERR . INTERNAL . show
|
|
|
|
notifySub' :: forall e m. (AEntityI e, MonadIO m) => AgentClient -> ConnId -> AEvent e -> m ()
|
|
notifySub' c connId cmd = liftIO $ nonBlockingWriteTBQueue (subQ c) (B.empty, connId, AEvt (sAEntity @e) cmd)
|
|
{-# INLINE notifySub' #-}
|
|
|
|
notifySub :: MonadIO m => AgentClient -> AEvent 'AENone -> m ()
|
|
notifySub c = notifySub' c ""
|
|
{-# INLINE notifySub #-}
|
|
|
|
getNtfServerClient :: AgentClient -> NetworkRequestMode -> NtfTransportSession -> AM NtfClient
|
|
getNtfServerClient c@AgentClient {active, ntfClients, workerSeq, proxySessTs, presetDomains} nm tSess@(_, srv, _) = do
|
|
unlessM (readTVarIO active) $ throwE INACTIVE
|
|
ts <- liftIO getCurrentTime
|
|
atomically (getSessVar workerSeq tSess ntfClients ts)
|
|
>>= either
|
|
(newProtocolClient c tSess ntfClients connectClient)
|
|
(waitForProtocolClient c nm tSess ntfClients)
|
|
where
|
|
connectClient :: NtfClientVar -> AM NtfClient
|
|
connectClient v = do
|
|
cfg <- lift $ getClientConfig c ntfCfg
|
|
g <- asks random
|
|
ts <- readTVarIO proxySessTs
|
|
liftError' (protocolClientError NTF $ B.unpack $ strEncode srv) $
|
|
getProtocolClient g nm tSess cfg presetDomains Nothing ts $
|
|
clientDisconnected v
|
|
|
|
clientDisconnected :: NtfClientVar -> NtfClient -> IO ()
|
|
clientDisconnected v client = do
|
|
atomically $ removeSessVar v tSess ntfClients
|
|
atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client)
|
|
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
|
|
|
getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient
|
|
getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq, proxySessTs, presetDomains} tSess@(_, srv, _) = do
|
|
unlessM (readTVarIO active) $ throwE INACTIVE
|
|
ts <- liftIO getCurrentTime
|
|
atomically (getSessVar workerSeq tSess xftpClients ts)
|
|
>>= either
|
|
(newProtocolClient c tSess xftpClients connectClient)
|
|
(waitForProtocolClient c NRMBackground tSess xftpClients)
|
|
where
|
|
connectClient :: XFTPClientVar -> AM XFTPClient
|
|
connectClient v = do
|
|
cfg <- asks $ xftpCfg . config
|
|
xftpNetworkConfig <- getNetworkConfig c
|
|
ts <- readTVarIO proxySessTs
|
|
liftError' (protocolClientError XFTP $ B.unpack $ strEncode srv) $
|
|
X.getXFTPClient tSess cfg {xftpNetworkConfig} presetDomains ts $
|
|
clientDisconnected v
|
|
|
|
clientDisconnected :: XFTPClientVar -> XFTPClient -> IO ()
|
|
clientDisconnected v client = do
|
|
atomically $ removeSessVar v tSess xftpClients
|
|
atomically $ writeTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent DISCONNECT client)
|
|
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
|
|
|
|
waitForProtocolClient ::
|
|
(ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) =>
|
|
AgentClient ->
|
|
NetworkRequestMode ->
|
|
TransportSession msg ->
|
|
TMap (TransportSession msg) (ClientVar msg) ->
|
|
ClientVar msg ->
|
|
AM (Client msg)
|
|
waitForProtocolClient c nm tSess@(_, srv, _) clients v = do
|
|
NetworkConfig {tcpConnectTimeout} <- getNetworkConfig c
|
|
client_ <- liftIO $ netTimeoutInt tcpConnectTimeout nm `timeout` atomically (readTMVar $ sessionVar v)
|
|
case client_ of
|
|
Just (Right smpClient) -> pure smpClient
|
|
Just (Left (e, ts_)) -> case ts_ of
|
|
Nothing -> throwE e
|
|
Just ts ->
|
|
ifM
|
|
((ts <) <$> liftIO getCurrentTime)
|
|
(atomically (removeSessVar v tSess clients) >> getProtocolServerClient c nm tSess)
|
|
(throwE e)
|
|
Nothing -> throwE $ BROKER (B.unpack $ strEncode srv) TIMEOUT
|
|
|
|
newProtocolClient ::
|
|
forall v err msg.
|
|
(ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) =>
|
|
AgentClient ->
|
|
TransportSession msg ->
|
|
TMap (TransportSession msg) (ClientVar msg) ->
|
|
(ClientVar msg -> AM (Client msg)) ->
|
|
ClientVar msg ->
|
|
AM (Client msg)
|
|
newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v =
|
|
tryAllErrors (connectClient v) >>= \case
|
|
Right client -> do
|
|
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv <> " (user " <> bshow userId <> maybe "" (" for entity " <>) entityId_ <> ")"
|
|
atomically $ putTMVar (sessionVar v) (Right client)
|
|
liftIO $ nonBlockingWriteTBQueue (subQ c) ("", "", AEvt SAENone $ hostEvent CONNECT client)
|
|
pure client
|
|
Left e -> do
|
|
ei <- asks $ persistErrorInterval . config
|
|
if ei == 0
|
|
then atomically $ do
|
|
removeSessVar v tSess clients
|
|
putTMVar (sessionVar v) (Left (e, Nothing))
|
|
else do
|
|
ts <- addUTCTime ei <$> liftIO getCurrentTime
|
|
atomically $ putTMVar (sessionVar v) (Left (e, Just ts))
|
|
throwE e -- signal error to caller
|
|
|
|
hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> AEvent 'AENone) -> Client msg -> AEvent 'AENone
|
|
hostEvent event = hostEvent' event . protocolClient
|
|
{-# INLINE hostEvent #-}
|
|
|
|
hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> AEvent 'AENone) -> ProtoClient msg -> AEvent 'AENone
|
|
hostEvent' event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost
|
|
|
|
getClientConfig :: AgentClient -> (AgentConfig -> ProtocolClientConfig v) -> AM' (ProtocolClientConfig v)
|
|
getClientConfig c cfgSel = do
|
|
cfg <- asks $ cfgSel . config
|
|
networkConfig <- getNetworkConfig c
|
|
pure cfg {networkConfig}
|
|
|
|
getNetworkConfig :: MonadIO m => AgentClient -> m NetworkConfig
|
|
getNetworkConfig c = do
|
|
(slowCfg, fastCfg) <- readTVarIO $ useNetworkConfig c
|
|
UserNetworkInfo {networkType} <- readTVarIO $ userNetworkInfo c
|
|
pure $ case networkType of
|
|
UNCellular -> slowCfg
|
|
UNNone -> slowCfg
|
|
_ -> fastCfg
|
|
|
|
-- returns fast network config
|
|
getFastNetworkConfig :: AgentClient -> IO NetworkConfig
|
|
getFastNetworkConfig = fmap snd . readTVarIO . useNetworkConfig
|
|
{-# INLINE getFastNetworkConfig #-}
|
|
|
|
waitForUserNetwork :: AgentClient -> IO ()
|
|
waitForUserNetwork c =
|
|
unlessM (isOnline <$> readTVarIO (userNetworkInfo c)) $ do
|
|
delay <- registerDelay $ userNetworkInterval $ config $ agentEnv c
|
|
atomically $ unlessM (isNetworkOnline c) $ unlessM (readTVar delay) retry
|
|
|
|
closeAgentClient :: AgentClient -> IO ()
|
|
closeAgentClient c = do
|
|
atomically $ writeTVar (active c) False
|
|
closeProtocolServerClients c smpClients
|
|
closeProtocolServerClients c ntfClients
|
|
closeProtocolServerClients c xftpClients
|
|
atomically $ writeTVar (smpProxiedRelays c) M.empty
|
|
atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect
|
|
clearWorkers smpDeliveryWorkers >>= mapM_ (cancelWorker . fst)
|
|
clearWorkers asyncCmdWorkers >>= mapM_ cancelWorker
|
|
atomically $ SS.clear $ currentSubs c
|
|
clear subscrConns
|
|
clear getMsgLocks
|
|
where
|
|
clearWorkers :: Ord k => (AgentClient -> TMap k a) -> IO (Map k a)
|
|
clearWorkers workers = atomically $ swapTVar (workers c) mempty
|
|
clear :: Monoid m => (AgentClient -> TVar m) -> IO ()
|
|
clear sel = atomically $ writeTVar (sel c) mempty
|
|
cancelReconnect :: SessionVar (Async ()) -> IO ()
|
|
cancelReconnect v = void . forkIO $ atomically (readTMVar $ sessionVar v) >>= uninterruptibleCancel
|
|
|
|
cancelWorker :: Worker -> IO ()
|
|
cancelWorker Worker {doWork, action} = do
|
|
noWorkToDo doWork
|
|
atomically (tryTakeTMVar action) >>= mapM_ (mapM_ $ deRefWeak >=> mapM_ killThread)
|
|
|
|
waitUntilActive :: AgentClient -> IO ()
|
|
waitUntilActive AgentClient {active} = unlessM (readTVarIO active) $ atomically $ unlessM (readTVar active) retry
|
|
|
|
throwWhenInactive :: AgentClient -> IO ()
|
|
throwWhenInactive c = unlessM (readTVarIO $ active c) $ E.throwIO ThreadKilled
|
|
{-# INLINE throwWhenInactive #-}
|
|
|
|
-- this function is used to remove workers once delivery is complete, not when it is removed from the map
|
|
throwWhenNoDelivery :: AgentClient -> SndQueue -> IO ()
|
|
throwWhenNoDelivery c sq =
|
|
unlessM (TM.memberIO (qAddress sq) $ smpDeliveryWorkers c) $
|
|
E.throwIO ThreadKilled
|
|
|
|
closeProtocolServerClients :: ProtocolServerClient v err msg => AgentClient -> (AgentClient -> TMap (TransportSession msg) (ClientVar msg)) -> IO ()
|
|
closeProtocolServerClients c clientsSel =
|
|
atomically (clientsSel c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient_ c)
|
|
|
|
reconnectServerClients :: ProtocolServerClient v err msg => AgentClient -> (AgentClient -> TMap (TransportSession msg) (ClientVar msg)) -> IO ()
|
|
reconnectServerClients c clientsSel =
|
|
readTVarIO (clientsSel c) >>= mapM_ (forkIO . closeClient_ c)
|
|
|
|
reconnectSMPServer :: AgentClient -> UserId -> SMPServer -> IO ()
|
|
reconnectSMPServer c userId srv = do
|
|
cs <- readTVarIO $ smpClients c
|
|
let vs = M.foldrWithKey srvClient [] cs
|
|
mapM_ (forkIO . closeClient_ c) vs
|
|
where
|
|
srvClient (userId', srv', _) v
|
|
| userId == userId' && srv == srv' = (v :)
|
|
| otherwise = id
|
|
|
|
closeClient :: ProtocolServerClient v err msg => AgentClient -> (AgentClient -> TMap (TransportSession msg) (ClientVar msg)) -> TransportSession msg -> IO ()
|
|
closeClient c clientSel tSess =
|
|
atomically (TM.lookupDelete tSess $ clientSel c) >>= mapM_ (closeClient_ c)
|
|
|
|
closeClient_ :: ProtocolServerClient v err msg => AgentClient -> ClientVar msg -> IO ()
|
|
closeClient_ c v = do
|
|
NetworkConfig {tcpConnectTimeout} <- getNetworkConfig c
|
|
E.handle (\BlockedIndefinitelyOnSTM -> pure ()) $
|
|
netTimeoutInt tcpConnectTimeout NRMBackground `timeout` atomically (readTMVar $ sessionVar v) >>= \case
|
|
Just (Right client) -> closeProtocolServerClient (protocolClient client) `catchAll_` pure ()
|
|
_ -> pure ()
|
|
|
|
closeXFTPServerClient :: AgentClient -> UserId -> XFTPServer -> FileDigest -> IO ()
|
|
closeXFTPServerClient c userId server (FileDigest chunkDigest) =
|
|
mkTransportSession c userId server chunkDigest >>= closeClient c xftpClients
|
|
|
|
withConnLock :: AgentClient -> ConnId -> Text -> AM a -> AM a
|
|
withConnLock c connId name = ExceptT . withConnLock' c connId name . runExceptT
|
|
{-# INLINE withConnLock #-}
|
|
|
|
withConnLock' :: AgentClient -> ConnId -> Text -> AM' a -> AM' a
|
|
withConnLock' _ "" _ = id
|
|
withConnLock' AgentClient {connLocks} connId name = withLockMap connLocks connId name
|
|
{-# INLINE withConnLock' #-}
|
|
|
|
withInvLock :: AgentClient -> ByteString -> Text -> AM a -> AM a
|
|
withInvLock c key name = ExceptT . withInvLock' c key name . runExceptT
|
|
{-# INLINE withInvLock #-}
|
|
|
|
withInvLock' :: AgentClient -> ByteString -> Text -> AM' a -> AM' a
|
|
withInvLock' AgentClient {invLocks} = withLockMap invLocks
|
|
{-# INLINE withInvLock' #-}
|
|
|
|
withConnLocks :: AgentClient -> Set ConnId -> Text -> AM' a -> AM' a
|
|
withConnLocks AgentClient {connLocks} = withLocksMap connLocks
|
|
{-# INLINE withConnLocks #-}
|
|
|
|
withLockMap :: (Ord k, MonadUnliftIO m) => TMap k Lock -> k -> Text -> m a -> m a
|
|
withLockMap = withGetLock . getMapLock
|
|
{-# INLINE withLockMap #-}
|
|
|
|
withLocksMap :: (Ord k, MonadUnliftIO m) => TMap k Lock -> Set k -> Text -> m a -> m a
|
|
withLocksMap = withGetLocks . getMapLock
|
|
{-# INLINE withLocksMap #-}
|
|
|
|
getMapLock :: Ord k => TMap k Lock -> k -> STM Lock
|
|
getMapLock locks key = TM.lookup key locks >>= maybe newLock pure
|
|
where
|
|
newLock = createLock >>= \l -> TM.insert key l locks $> l
|
|
|
|
withClient_ :: forall a v err msg. ProtocolServerClient v err msg => AgentClient -> NetworkRequestMode -> TransportSession msg -> (Client msg -> AM a) -> AM a
|
|
withClient_ c nm tSess@(_, srv, _) action = do
|
|
cl <- getProtocolServerClient c nm tSess
|
|
action cl `catchAllErrors` logServerError
|
|
where
|
|
logServerError :: AgentErrorType -> AM a
|
|
logServerError e = do
|
|
logServer "<--" c srv NoEntity $ bshow e
|
|
throwE e
|
|
|
|
withProxySession :: AgentClient -> NetworkRequestMode -> Maybe SMPServerWithAuth -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a
|
|
withProxySession c nm proxySrv_ destSess@(_, destSrv, _) entId cmdStr action = do
|
|
(cl, sess_) <- getSMPProxyClient c nm proxySrv_ destSess
|
|
logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr
|
|
case sess_ of
|
|
Right sess -> do
|
|
r <- action (cl, sess) `catchAllErrors` logServerError cl
|
|
logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK"
|
|
pure r
|
|
Left e -> logServerError cl e
|
|
where
|
|
proxySrv = showServer . protocolClientServer' . protocolClient
|
|
logServerError :: SMPConnectedClient -> AgentErrorType -> AM a
|
|
logServerError cl e = do
|
|
logServer ("<-- " <> proxySrv cl <> " <") c destSrv NoEntity $ bshow e
|
|
throwE e
|
|
|
|
withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> NetworkRequestMode -> TransportSession msg -> ByteString -> ByteString -> (Client msg -> AM a) -> AM a
|
|
withLogClient_ c nm tSess@(_, srv, _) entId cmdStr action = do
|
|
logServer' "-->" c srv entId cmdStr
|
|
res <- withClient_ c nm tSess action
|
|
logServer' "<--" c srv entId "OK"
|
|
return res
|
|
|
|
withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> NetworkRequestMode -> TransportSession msg -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a
|
|
withClient c nm tSess action = withClient_ c nm tSess $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client
|
|
{-# INLINE withClient #-}
|
|
|
|
withLogClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> NetworkRequestMode -> TransportSession msg -> ByteString -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a
|
|
withLogClient c nm tSess entId cmdStr action = withLogClient_ c nm tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client
|
|
{-# INLINE withLogClient #-}
|
|
|
|
withSMPClient :: SMPQueueRec q => AgentClient -> NetworkRequestMode -> q -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> AM a
|
|
withSMPClient c nm q cmdStr action = do
|
|
tSess <- mkSMPTransportSessionIO c q
|
|
withLogClient c nm tSess (unEntityId $ queueId q) cmdStr $ action . connectedClient
|
|
|
|
sendOrProxySMPMessage :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> ConnId -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer)
|
|
sendOrProxySMPMessage c nm userId destSrv connId cmdStr spKey_ senderId msgFlags msg =
|
|
fst <$> sendOrProxySMPCommand c nm userId destSrv connId cmdStr senderId sendViaProxy sendDirectly
|
|
where
|
|
sendViaProxy smp proxySess = do
|
|
atomically $ incSMPServerStat c userId destSrv sentViaProxyAttempts
|
|
atomically $ incSMPServerStat c userId (protocolClientServer' smp) sentProxiedAttempts
|
|
proxySMPMessage smp nm proxySess spKey_ senderId msgFlags msg
|
|
sendDirectly smp = do
|
|
atomically $ incSMPServerStat c userId destSrv sentDirectAttempts
|
|
sendSMPMessage smp nm spKey_ senderId msgFlags msg
|
|
|
|
sendOrProxySMPCommand ::
|
|
forall a.
|
|
AgentClient ->
|
|
NetworkRequestMode ->
|
|
UserId ->
|
|
SMPServer ->
|
|
ConnId -> -- session entity ID, for short links LinkId is used
|
|
ByteString ->
|
|
SMP.EntityId -> -- sender or link ID
|
|
(SMPClient -> ProxiedRelay -> ExceptT SMPClientError IO (Either ProxyClientError a)) ->
|
|
(SMPClient -> ExceptT SMPClientError IO a) ->
|
|
AM (Maybe SMPServer, a)
|
|
sendOrProxySMPCommand c nm userId destSrv@ProtocolServer {host = destHosts} connId cmdStr entId sendCmdViaProxy sendCmdDirectly = do
|
|
tSess <- mkTransportSession c userId destSrv connId
|
|
ifM shouldUseProxy (sendViaProxy Nothing tSess) ((Nothing,) <$> sendDirectly tSess)
|
|
where
|
|
shouldUseProxy = do
|
|
cfg <- getNetworkConfig c
|
|
case smpProxyMode cfg of
|
|
SPMAlways -> pure True
|
|
SPMUnknown -> unknownServer
|
|
SPMUnprotected
|
|
| ipAddressProtected cfg destSrv -> pure False
|
|
| otherwise -> unknownServer
|
|
SPMNever -> pure False
|
|
directAllowed = do
|
|
cfg <- getNetworkConfig c
|
|
pure $ case smpProxyFallback cfg of
|
|
SPFAllow -> True
|
|
SPFAllowProtected -> ipAddressProtected cfg destSrv
|
|
SPFProhibit -> False
|
|
unknownServer = liftIO $ maybe True (\srvs -> all (`S.notMember` knownHosts srvs) destHosts) <$> TM.lookupIO userId (smpServers c)
|
|
sendViaProxy :: Maybe SMPServerWithAuth -> SMPTransportSession -> AM (Maybe SMPServer, a)
|
|
sendViaProxy proxySrv_ destSess@(_, _, connId_) = do
|
|
r <- tryAllErrors . withProxySession c nm proxySrv_ destSess entId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess@ProxiedRelay {prBasicAuth}) -> do
|
|
r' <- liftClient SMP (clientServer smp) $ sendCmdViaProxy smp proxySess
|
|
let proxySrv = protocolClientServer' smp
|
|
case r' of
|
|
Right r -> pure (Just proxySrv, r)
|
|
Left proxyErr -> do
|
|
case proxyErr of
|
|
ProxyProtocolError (SMP.PROXY SMP.NO_SESSION) -> do
|
|
atomically deleteRelaySession
|
|
case proxySrv_ of
|
|
Just _ -> proxyError
|
|
-- sendViaProxy is called recursively here to re-create the session via the same server
|
|
-- to avoid failure in interactive calls that don't retry after the session disconnection.
|
|
Nothing -> sendViaProxy (Just $ ProtoServerWithAuth proxySrv prBasicAuth) destSess
|
|
_ -> proxyError
|
|
where
|
|
proxyError =
|
|
throwE
|
|
PROXY
|
|
{ proxyServer = protocolClientServer smp,
|
|
relayServer = B.unpack $ strEncode destSrv,
|
|
proxyErr
|
|
}
|
|
-- checks that the current proxied relay session is the same one that was used to send the message and removes it
|
|
deleteRelaySession =
|
|
( TM.lookup destSess (smpProxiedRelays c)
|
|
$>>= \(ProtoServerWithAuth srv _) -> tryReadSessVar (userId, srv, connId_) (smpClients c)
|
|
)
|
|
>>= \case
|
|
Just (Right (SMPConnectedClient smp' prs))
|
|
| sameClient smp' ->
|
|
tryReadSessVar destSrv prs >>= \case
|
|
Just (Right proxySess') | sameProxiedRelay proxySess' -> TM.delete destSrv prs
|
|
_ -> pure ()
|
|
_ -> pure ()
|
|
sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp')
|
|
sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess'
|
|
case r of
|
|
Right r'@(srv_, _) -> do
|
|
atomically $ incSMPServerStat c userId destSrv sentViaProxy
|
|
forM_ srv_ $ \proxySrv -> atomically $ incSMPServerStat c userId proxySrv sentProxied
|
|
pure r'
|
|
Left e
|
|
| serverHostError e -> ifM directAllowed ((Nothing,) <$> sendDirectly destSess) (throwE e)
|
|
| otherwise -> throwE e
|
|
sendDirectly tSess =
|
|
withLogClient_ c nm tSess (unEntityId entId) ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> do
|
|
tryAllErrors (liftClient SMP (clientServer smp) $ sendCmdDirectly smp) >>= \case
|
|
Right r -> r <$ atomically (incSMPServerStat c userId destSrv sentDirect)
|
|
Left e -> throwE e
|
|
|
|
ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool
|
|
ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do
|
|
isJust socksProxy || (hostMode == HMOnion && any isOnionHost hosts)
|
|
where
|
|
isOnionHost = \case THOnionHost _ -> True; _ -> False
|
|
|
|
withNtfClient :: AgentClient -> NetworkRequestMode -> NtfServer -> EntityId -> ByteString -> (NtfClient -> ExceptT NtfClientError IO a) -> AM a
|
|
withNtfClient c nm srv (EntityId entId) = withLogClient c nm (0, srv, Nothing) entId
|
|
|
|
withXFTPClient ::
|
|
ProtocolServerClient v err msg =>
|
|
AgentClient ->
|
|
(UserId, ProtoServer msg, ByteString) ->
|
|
ByteString ->
|
|
(Client msg -> ExceptT (ProtocolClientError err) IO b) ->
|
|
AM b
|
|
withXFTPClient c (userId, srv, sessEntId) cmdStr action = do
|
|
tSess <- mkTransportSession c userId srv sessEntId
|
|
withLogClient c NRMBackground tSess sessEntId cmdStr action
|
|
|
|
liftClient :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ExceptT (ProtocolClientError err) IO a -> AM a
|
|
liftClient protocolError_ = liftError . protocolClientError protocolError_
|
|
{-# INLINE liftClient #-}
|
|
|
|
protocolClientError :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ProtocolClientError err -> AgentErrorType
|
|
protocolClientError protocolError_ host = \case
|
|
PCEProtocolError e -> protocolError_ host e
|
|
PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e
|
|
PCEUnexpectedResponse e -> BROKER host $ UNEXPECTED $ B.unpack e
|
|
PCEResponseTimeout -> BROKER host TIMEOUT
|
|
PCENetworkError e -> BROKER host $ NETWORK e
|
|
PCEIncompatibleHost -> BROKER host HOST
|
|
PCETransportError e -> BROKER host $ TRANSPORT e
|
|
e@PCECryptoError {} -> INTERNAL $ show e
|
|
PCEServiceUnavailable {} -> BROKER host NO_SERVICE
|
|
PCEIOError e -> BROKER host $ NETWORK $ NEConnectError e
|
|
|
|
-- it is consistent with smpClientServiceError
|
|
clientServiceError :: AgentErrorType -> Bool
|
|
clientServiceError = \case
|
|
BROKER _ NO_SERVICE -> True
|
|
SMP _ SMP.SERVICE -> True
|
|
SMP _ (SMP.PROXY (SMP.BROKER NO_SERVICE)) -> True -- for completeness, it cannot happen.
|
|
_ -> False
|
|
|
|
data ProtocolTestStep
|
|
= TSConnect
|
|
| TSDisconnect
|
|
| TSCreateQueue
|
|
| TSSecureQueue
|
|
| TSDeleteQueue
|
|
| TSCreateFile
|
|
| TSUploadFile
|
|
| TSDownloadFile
|
|
| TSCompareFile
|
|
| TSDeleteFile
|
|
| TSCreateNtfToken
|
|
| TSDeleteNtfToken
|
|
deriving (Eq, Show)
|
|
|
|
data ProtocolTestFailure = ProtocolTestFailure
|
|
{ testStep :: ProtocolTestStep,
|
|
testError :: AgentErrorType
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
runSMPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> SMPServerWithAuth -> AM' (Maybe ProtocolTestFailure)
|
|
runSMPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth srv auth) = do
|
|
cfg <- getClientConfig c smpCfg
|
|
C.AuthAlg ra <- asks $ rcvAuthAlg . config
|
|
C.AuthAlg sa <- asks $ sndAuthAlg . config
|
|
g <- asks random
|
|
liftIO $ do
|
|
let tSess = (userId, srv, Nothing)
|
|
ts <- readTVarIO $ proxySessTs c
|
|
getProtocolClient g nm tSess cfg presetDomains Nothing ts (\_ -> pure ()) >>= \case
|
|
Right smp -> do
|
|
rKeys@(_, rpKey) <- atomically $ C.generateAuthKeyPair ra g
|
|
(sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g
|
|
(dhKey, _) <- atomically $ C.generateKeyPair g
|
|
r <- runExceptT $ do
|
|
SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp nm Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) Nothing
|
|
liftError (testErr TSSecureQueue) $
|
|
case queueMode of
|
|
Just QMMessaging -> secureSndSMPQueue smp nm spKey sndId
|
|
_ -> secureSMPQueue smp nm rpKey rcvId sKey
|
|
liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp nm rpKey rcvId
|
|
ok <- netTimeoutInt (tcpTimeout $ networkConfig cfg) nm `timeout` closeProtocolClient smp
|
|
pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok
|
|
Left e -> pure (Just $ testErr TSConnect e)
|
|
where
|
|
addr = B.unpack $ strEncode srv
|
|
testErr :: ProtocolTestStep -> SMPClientError -> ProtocolTestFailure
|
|
testErr step = ProtocolTestFailure step . protocolClientError SMP addr
|
|
|
|
runXFTPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> XFTPServerWithAuth -> AM' (Maybe ProtocolTestFailure)
|
|
runXFTPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth srv auth) = do
|
|
cfg <- asks $ xftpCfg . config
|
|
g <- asks random
|
|
xftpNetworkConfig <- getNetworkConfig c
|
|
workDir <- getXFTPWorkPath
|
|
filePath <- getTempFilePath workDir
|
|
rcvPath <- getTempFilePath workDir
|
|
liftIO $ do
|
|
let tSess = (userId, srv, Nothing)
|
|
ts <- readTVarIO $ proxySessTs c
|
|
X.getXFTPClient tSess cfg {xftpNetworkConfig} presetDomains ts (\_ -> pure ()) >>= \case
|
|
Right xftp -> withTestChunk filePath $ do
|
|
(sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
(rcvKey, rpKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
|
digest <- liftIO $ C.sha256Hash <$> B.readFile filePath
|
|
let file = FileInfo {sndKey, size = chSize, digest}
|
|
chunkSpec = X.XFTPChunkSpec {filePath, chunkOffset = 0, chunkSize = chSize}
|
|
r <- runExceptT $ do
|
|
(sId, [rId]) <- liftError (testErr TSCreateFile) $ X.createXFTPChunk xftp spKey file [rcvKey] auth
|
|
liftError (testErr TSUploadFile) $ X.uploadXFTPChunk xftp spKey sId chunkSpec
|
|
liftError (testErr TSDownloadFile) $ X.downloadXFTPChunk g xftp rpKey rId $ XFTPRcvChunkSpec rcvPath chSize digest
|
|
rcvDigest <- liftIO $ C.sha256Hash <$> B.readFile rcvPath
|
|
unless (digest == rcvDigest) $ throwE $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST
|
|
liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId
|
|
ok <- netTimeoutInt (tcpTimeout xftpNetworkConfig) nm `timeout` X.closeXFTPClient xftp
|
|
pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok
|
|
Left e -> pure (Just $ testErr TSConnect e)
|
|
where
|
|
addr = B.unpack $ strEncode srv
|
|
testErr :: ProtocolTestStep -> XFTPClientError -> ProtocolTestFailure
|
|
testErr step = ProtocolTestFailure step . protocolClientError XFTP addr
|
|
chSize :: Integral a => a
|
|
chSize = kb 64
|
|
getTempFilePath :: FilePath -> AM' FilePath
|
|
getTempFilePath workPath = do
|
|
ts <- liftIO getCurrentTime
|
|
let isoTime = formatTime defaultTimeLocale "%Y-%m-%dT%H%M%S.%6q" ts
|
|
uniqueCombine workPath isoTime
|
|
withTestChunk :: FilePath -> IO a -> IO a
|
|
withTestChunk fp =
|
|
E.bracket_
|
|
(createTestChunk fp)
|
|
(whenM (doesFileExist fp) $ removeFile fp `catchAll_` pure ())
|
|
-- this creates a new DRG on purpose to avoid blocking the one used in the agent
|
|
createTestChunk :: FilePath -> IO ()
|
|
createTestChunk fp = B.writeFile fp =<< atomically . C.randomBytes chSize =<< C.newRandom
|
|
|
|
runNTFServerTest :: AgentClient -> NetworkRequestMode -> UserId -> NtfServerWithAuth -> AM' (Maybe ProtocolTestFailure)
|
|
runNTFServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth srv _) = do
|
|
cfg <- getClientConfig c ntfCfg
|
|
C.AuthAlg a <- asks $ rcvAuthAlg . config
|
|
g <- asks random
|
|
liftIO $ do
|
|
let tSess = (userId, srv, Nothing)
|
|
ts <- readTVarIO $ proxySessTs c
|
|
getProtocolClient g nm tSess cfg presetDomains Nothing ts (\_ -> pure ()) >>= \case
|
|
Right ntf -> do
|
|
(nKey, npKey) <- atomically $ C.generateAuthKeyPair a g
|
|
(dhKey, _) <- atomically $ C.generateKeyPair g
|
|
r <- runExceptT $ do
|
|
let deviceToken = DeviceToken PPApnsNull "test_ntf_token"
|
|
(tknId, _) <- liftError (testErr TSCreateNtfToken) $ ntfRegisterToken ntf nm npKey (NewNtfTkn deviceToken nKey dhKey)
|
|
liftError (testErr TSDeleteNtfToken) $ ntfDeleteToken ntf nm npKey tknId
|
|
ok <- netTimeoutInt (tcpTimeout $ networkConfig cfg) nm `timeout` closeProtocolClient ntf
|
|
pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok
|
|
Left e -> pure (Just $ testErr TSConnect e)
|
|
where
|
|
addr = B.unpack $ strEncode srv
|
|
testErr :: ProtocolTestStep -> SMPClientError -> ProtocolTestFailure
|
|
testErr step = ProtocolTestFailure step . protocolClientError NTF addr
|
|
|
|
getXFTPWorkPath :: AM' FilePath
|
|
getXFTPWorkPath = do
|
|
workDir <- readTVarIO =<< asks (xftpWorkDir . xftpAgent)
|
|
maybe getTemporaryDirectory pure workDir
|
|
|
|
mkTransportSession :: MonadIO m => AgentClient -> UserId -> ProtoServer msg -> ByteString -> m (TransportSession msg)
|
|
mkTransportSession c userId srv sessEntId = mkTSession userId srv sessEntId <$> getSessionModeIO c
|
|
{-# INLINE mkTransportSession #-}
|
|
|
|
mkTSession :: UserId -> ProtoServer msg -> ByteString -> TransportSessionMode -> TransportSession msg
|
|
mkTSession userId srv sessEntId mode = (userId, srv, if mode == TSMEntity then Just sessEntId else Nothing)
|
|
{-# INLINE mkTSession #-}
|
|
|
|
mkSMPTransportSessionIO :: (SMPQueueRec q, MonadIO m) => AgentClient -> q -> m SMPTransportSession
|
|
mkSMPTransportSessionIO c q = mkSMPTSession q <$> getSessionModeIO c
|
|
{-# INLINE mkSMPTransportSessionIO #-}
|
|
|
|
mkSMPTransportSession :: SMPQueueRec q => AgentClient -> q -> STM SMPTransportSession
|
|
mkSMPTransportSession c q = mkSMPTSession q <$> getSessionMode c
|
|
{-# INLINE mkSMPTransportSession #-}
|
|
|
|
mkSMPTSession :: SMPQueueRec q => q -> TransportSessionMode -> SMPTransportSession
|
|
mkSMPTSession q = mkTSession (qUserId q) (qServer q) (qConnId q)
|
|
{-# INLINE mkSMPTSession #-}
|
|
|
|
getSessionModeIO :: MonadIO m => AgentClient -> m TransportSessionMode
|
|
getSessionModeIO = fmap (sessionMode . snd) . readTVarIO . useNetworkConfig
|
|
{-# INLINE getSessionModeIO #-}
|
|
|
|
getSessionMode :: AgentClient -> STM TransportSessionMode
|
|
getSessionMode = fmap (sessionMode . snd) . readTVar . useNetworkConfig
|
|
{-# INLINE getSessionMode #-}
|
|
|
|
newRcvQueue :: AgentClient -> NetworkRequestMode -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> Bool -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId, Maybe ServiceId)
|
|
newRcvQueue c nm userId connId srv vRange cMode enableNtfs subMode = do
|
|
let qrd = case cMode of SCMInvitation -> CQRMessaging Nothing; SCMContact -> CQRContact Nothing
|
|
e2eKeys <- atomically . C.generateKeyPair =<< asks random
|
|
newRcvQueue_ c nm userId connId srv vRange qrd enableNtfs subMode Nothing e2eKeys
|
|
|
|
data ClntQueueReqData
|
|
= CQRMessaging (Maybe (CQRData (SMP.SenderId, QueueLinkData)))
|
|
| CQRContact (Maybe (CQRData (SMP.LinkId, (SMP.SenderId, QueueLinkData))))
|
|
|
|
data CQRData r = CQRData
|
|
{ linkKey :: LinkKey,
|
|
privSigKey :: C.PrivateKeyEd25519,
|
|
srvReq :: r
|
|
}
|
|
|
|
queueReqData :: ClntQueueReqData -> QueueReqData
|
|
queueReqData = \case
|
|
CQRMessaging d -> QRMessaging $ srvReq <$> d
|
|
CQRContact d -> QRContact $ srvReq <$> d
|
|
|
|
newRcvQueue_ :: AgentClient -> NetworkRequestMode -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> Bool -> SubscriptionMode -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId, Maybe ServiceId)
|
|
newRcvQueue_ c nm userId connId (ProtoServerWithAuth srv auth) vRange cqrd enableNtfs subMode nonce_ (e2eDhKey, e2ePrivKey) = do
|
|
C.AuthAlg a <- asks (rcvAuthAlg . config)
|
|
g <- asks random
|
|
rKeys@(_, rcvPrivateKey) <- atomically $ C.generateAuthKeyPair a g
|
|
(dhKey, privDhKey) <- atomically $ C.generateKeyPair g
|
|
logServer "-->" c srv NoEntity "NEW"
|
|
tSess <- mkTransportSession c userId srv connId
|
|
(thParams', ntfKeys, qik@QIK {rcvId, sndId, rcvPublicDhKey, queueMode, serviceId, serverNtfCreds}) <-
|
|
withClient c nm tSess $ \(SMPConnectedClient smp _) -> do
|
|
(ntfKeys, ntfCreds) <- liftIO $ mkNtfCreds a g smp
|
|
(thParams smp,ntfKeys,) <$> createSMPQueue smp nm nonce_ rKeys dhKey auth subMode (queueReqData cqrd) ntfCreds
|
|
let sessServiceId = (\THClientService {serviceId = sId} -> sId) <$> (clientService =<< thAuth thParams')
|
|
when (isJust serviceId && serviceId /= sessServiceId) $ logError "incorrect service ID in NEW response"
|
|
liftIO . logServer "<--" c srv NoEntity $ B.unwords ["IDS", logSecret rcvId, logSecret sndId]
|
|
shortLink <- mkShortLinkCreds thParams' qik
|
|
let rq =
|
|
RcvQueue
|
|
{ userId,
|
|
connId,
|
|
server = srv,
|
|
rcvId,
|
|
rcvPrivateKey,
|
|
rcvDhSecret = C.dh' rcvPublicDhKey privDhKey,
|
|
e2ePrivKey,
|
|
e2eDhSecret = Nothing,
|
|
sndId,
|
|
queueMode,
|
|
shortLink,
|
|
rcvServiceAssoc = isJust serviceId && serviceId == sessServiceId,
|
|
status = New,
|
|
enableNtfs,
|
|
clientNoticeId = Nothing,
|
|
dbQueueId = DBNewEntity,
|
|
primary = True,
|
|
dbReplaceQueueId = Nothing,
|
|
rcvSwchStatus = Nothing,
|
|
smpClientVersion = maxVersion vRange,
|
|
clientNtfCreds = mkClientNtfCreds ntfKeys serverNtfCreds,
|
|
deleteErrors = 0
|
|
}
|
|
qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey queueMode
|
|
pure (rq, qUri, tSess, sessionId thParams', sessServiceId)
|
|
where
|
|
mkNtfCreds :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> TVar ChaChaDRG -> SMPClient -> IO (Maybe (C.AAuthKeyPair, C.PrivateKeyX25519), Maybe NewNtfCreds)
|
|
mkNtfCreds a g smp
|
|
| enableNtfs && thVersion (thParams smp) >= newNtfCredsSMPVersion = do
|
|
authKeys@(k, _) <- atomically $ C.generateAuthKeyPair a g
|
|
(dhk, dhpk) <- atomically $ C.generateKeyPair g
|
|
pure (Just (authKeys, dhpk), Just $ NewNtfCreds k dhk)
|
|
| otherwise = pure (Nothing, Nothing)
|
|
mkClientNtfCreds :: Maybe (C.AAuthKeyPair, C.PrivateKeyX25519) -> Maybe ServerNtfCreds -> Maybe ClientNtfCreds
|
|
mkClientNtfCreds ntfKeys serverNtfCreds = case (ntfKeys, serverNtfCreds) of
|
|
(Just ((ntfPublicKey, ntfPrivateKey), dhpk), Just (ServerNtfCreds notifierId dhk')) ->
|
|
Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret = C.dh' dhk' dhpk}
|
|
_ -> Nothing
|
|
mkShortLinkCreds :: THandleParams SMPVersion 'TClient -> QueueIdsKeys -> AM (Maybe ShortLinkCreds)
|
|
mkShortLinkCreds thParams' QIK {sndId, queueMode, linkId} = case (cqrd, queueMode) of
|
|
(CQRMessaging ld, Just QMMessaging) ->
|
|
withLinkData ld $ \lnkId CQRData {linkKey, privSigKey, srvReq = (sndId', d)} ->
|
|
if sndId == sndId'
|
|
then pure $ Just $ ShortLinkCreds lnkId linkKey privSigKey Nothing (fst d)
|
|
else newErr "different sender ID"
|
|
(CQRContact ld, Just QMContact) ->
|
|
withLinkData ld $ \lnkId CQRData {linkKey, privSigKey, srvReq = (lnkId', (sndId', d))} ->
|
|
if sndId == sndId' && lnkId == lnkId'
|
|
then pure $ Just $ ShortLinkCreds lnkId linkKey privSigKey Nothing (fst d)
|
|
else newErr "different sender or link IDs"
|
|
(_, Nothing) -> case linkId of
|
|
Nothing | v < sndAuthKeySMPVersion -> pure Nothing
|
|
_ -> newErr "unexpected link ID"
|
|
_ -> newErr "unexpected queue mode"
|
|
where
|
|
v = thVersion thParams'
|
|
withLinkData :: Maybe d -> (SMP.LinkId -> d -> AM (Maybe ShortLinkCreds)) -> AM (Maybe ShortLinkCreds)
|
|
withLinkData ld_ mkLink = case (ld_, linkId) of
|
|
(Just ld, Just lnkId) -> mkLink lnkId ld
|
|
(Just _, Nothing) | v < shortLinksSMPVersion -> pure Nothing
|
|
(Nothing, Nothing) -> pure Nothing
|
|
_ -> newErr "unexpected or absent link ID"
|
|
newErr :: String -> AM (Maybe ShortLinkCreds)
|
|
newErr = throwE . BROKER (B.unpack $ strEncode srv) . UNEXPECTED . ("Create queue: " <>)
|
|
|
|
processSubResults :: AgentClient -> SMPTransportSession -> SessionId -> Maybe ServiceId -> NonEmpty (RcvQueueSub, Either SMPClientError (Maybe ServiceId)) -> STM ([RcvQueueSub], [(RcvQueueSub, Maybe ClientNotice)])
|
|
processSubResults c tSess@(userId, srv, _) sessId serviceId_ rs = do
|
|
pendingSubs <- SS.getPendingQueueSubs tSess $ currentSubs c
|
|
let (failed, subscribed@(qs, sQs), notices, ignored) = foldr (partitionResults pendingSubs) (M.empty, ([], []), [], 0) rs
|
|
unless (M.null failed) $ do
|
|
incSMPServerStat' c userId srv connSubErrs $ M.size failed
|
|
failSubscriptions c tSess failed
|
|
unless (null qs && null sQs) $ do
|
|
incSMPServerStat' c userId srv connSubscribed $ length qs + length sQs
|
|
SS.batchAddActiveSubs tSess sessId serviceId_ subscribed $ currentSubs c
|
|
unless (ignored == 0) $ incSMPServerStat' c userId srv connSubIgnored ignored
|
|
pure (sQs, notices)
|
|
where
|
|
partitionResults ::
|
|
Map SMP.RecipientId RcvQueueSub ->
|
|
(RcvQueueSub, Either SMPClientError (Maybe ServiceId)) ->
|
|
(Map SMP.RecipientId SMPClientError, ([RcvQueueSub], [RcvQueueSub]), [(RcvQueueSub, Maybe ClientNotice)], Int) ->
|
|
(Map SMP.RecipientId SMPClientError, ([RcvQueueSub], [RcvQueueSub]), [(RcvQueueSub, Maybe ClientNotice)], Int)
|
|
partitionResults pendingSubs (rq@RcvQueueSub {rcvId, clientNoticeId}, r) acc@(failed, subscribed@(qs, sQs), notices, ignored) = case r of
|
|
Left e -> case smpErrorClientNotice e of
|
|
Just notice_ -> (failed', subscribed, notices', ignored)
|
|
where
|
|
notices' = if isJust notice_ || isJust clientNoticeId then (rq, notice_) : notices else notices
|
|
Nothing
|
|
| temporaryClientError e -> acc
|
|
| otherwise -> (failed', subscribed, notices, ignored)
|
|
where
|
|
failed' = M.insert rcvId e failed
|
|
Right serviceId_'
|
|
| rcvId `M.member` pendingSubs ->
|
|
let subscribed' = case (serviceId_, serviceId_') of
|
|
(Just sId, Just sId') | sId == sId' -> (qs, rq : sQs)
|
|
_ -> (rq : qs, sQs)
|
|
in (failed, subscribed', notices', ignored)
|
|
| otherwise -> (failed, subscribed, notices', ignored + 1)
|
|
where
|
|
notices' = if isJust clientNoticeId then (rq, Nothing) : notices else notices
|
|
|
|
temporaryAgentError :: AgentErrorType -> Bool
|
|
temporaryAgentError = \case
|
|
BROKER _ e -> tempBrokerError e
|
|
SMP _ (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e
|
|
SMP _ (SMP.STORE _) -> True
|
|
NTF _ (SMP.STORE _) -> True
|
|
XFTP _ XFTP.TIMEOUT -> True
|
|
PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e
|
|
PROXY _ _ (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> True
|
|
INACTIVE -> True
|
|
CRITICAL True _ -> True -- critical errors that do not show restart button are likely to be permanent
|
|
_ -> False
|
|
where
|
|
tempBrokerError = \case
|
|
NETWORK _ -> True
|
|
TIMEOUT -> True
|
|
TRANSPORT (TEHandshake BAD_SERVICE) -> True -- this error is considered temporary because it is DB error
|
|
_ -> False
|
|
|
|
temporaryOrHostError :: AgentErrorType -> Bool
|
|
temporaryOrHostError e = temporaryAgentError e || serverHostError e
|
|
{-# INLINE temporaryOrHostError #-}
|
|
|
|
serverHostError :: AgentErrorType -> Bool
|
|
serverHostError = \case
|
|
BROKER _ e -> brokerHostError e
|
|
SMP _ (SMP.PROXY (SMP.BROKER e)) -> brokerHostError e
|
|
PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerHostError e
|
|
_ -> False
|
|
where
|
|
brokerHostError = \case
|
|
HOST -> True
|
|
SMP.TRANSPORT TEVersion -> True
|
|
_ -> False
|
|
|
|
-- | Batch by transport session and subscribe queues. The list of results can have a different order.
|
|
subscribeQueues :: AgentClient -> Bool -> [RcvQueueSub] -> AM' [(RcvQueueSub, Either AgentErrorType (Maybe ServiceId))]
|
|
subscribeQueues _ _ [] = pure []
|
|
subscribeQueues c withEvents qs = do
|
|
(errs, qs') <- checkQueues c qs
|
|
atomically $ modifyTVar' (subscrConns c) (`S.union` S.fromList (map qConnId qs'))
|
|
qss <- batchQueues mkSMPTSession qs' <$> getSessionModeIO c
|
|
mapM_ (addPendingSubs c) qss
|
|
rs <- mapConcurrently (subscribeQueues_ c withEvents) qss
|
|
when withEvents $ forM_ (L.nonEmpty errs) $ notifySub c . ERRS . L.map (first qConnId)
|
|
pure $ map (second Left) errs <> concatMap L.toList rs
|
|
|
|
addPendingSubs :: AgentClient -> (SMPTransportSession, NonEmpty RcvQueueSub) -> AM' ()
|
|
addPendingSubs c (tSess, qs') = atomically $ SS.batchAddPendingSubs tSess (L.toList qs') $ currentSubs c
|
|
|
|
subscribeQueues_ :: AgentClient -> Bool -> (SMPTransportSession, NonEmpty RcvQueueSub) -> AM' (BatchResponses RcvQueueSub AgentErrorType (Maybe ServiceId))
|
|
subscribeQueues_ c withEvents qs'@(tSess@(_, srv, _), _) = do
|
|
(rs, active) <- subscribeSessQueues_ c withEvents qs'
|
|
if active
|
|
then when (hasTempErrors rs) resubscribe $> rs
|
|
else do
|
|
logWarn "subcription batch result for replaced SMP client, resubscribing"
|
|
-- we use BROKER NETWORK error here instead of the original error, so it becomes temporary.
|
|
resubscribe $> L.map (second $ Left . toNESubscribeError) rs
|
|
where
|
|
-- treating host errors as temporary here as well
|
|
hasTempErrors = any (either temporaryOrHostError (const False) . snd)
|
|
toNESubscribeError = BROKER (B.unpack $ strEncode srv) . NETWORK . NESubscribeError . show
|
|
resubscribe = resubscribeSMPSession c tSess
|
|
|
|
subscribeUserServerQueues :: AgentClient -> UserId -> SMPServer -> [RcvQueueSub] -> AM' [(RcvQueueSub, Either AgentErrorType (Maybe ServiceId))]
|
|
subscribeUserServerQueues c userId srv qs = do
|
|
mode <- getSessionModeIO c
|
|
if mode == TSMEntity
|
|
then subscribeQueues c True qs
|
|
else do
|
|
let tSess = (userId, srv, Nothing)
|
|
(errs, qs_) <- checkQueues c qs
|
|
forM_ (L.nonEmpty errs) $ notifySub c . ERRS . L.map (first qConnId)
|
|
let errs' = map (second Left) errs
|
|
case L.nonEmpty qs_ of
|
|
Just qs' -> do
|
|
atomically $ modifyTVar' (subscrConns c) (`S.union` S.fromList (map qConnId $ L.toList qs'))
|
|
addPendingSubs c (tSess, qs')
|
|
rs <- subscribeQueues_ c True (tSess, qs')
|
|
pure $ errs' <> L.toList rs
|
|
Nothing -> pure errs'
|
|
|
|
-- only "checked" queues are subscribed
|
|
checkQueues :: AgentClient -> [RcvQueueSub] -> AM' ([(RcvQueueSub, AgentErrorType)], [RcvQueueSub])
|
|
checkQueues c = fmap partitionEithers . mapM checkQueue
|
|
where
|
|
checkQueue rq = do
|
|
prohibited <- liftIO $ hasGetLock c rq
|
|
pure $ if prohibited then Left (rq, CMD PROHIBITED "checkQueues") else Right rq
|
|
|
|
-- This function expects that all queues belong to one transport session,
|
|
-- and that they are already added to pending subscriptions.
|
|
resubscribeSessQueues :: AgentClient -> SMPTransportSession -> [RcvQueueSub] -> AM' ()
|
|
resubscribeSessQueues _ _ [] = pure ()
|
|
resubscribeSessQueues c tSess qs = do
|
|
(errs, qs_) <- checkQueues c qs
|
|
forM_ (L.nonEmpty qs_) $ \qs' -> void $ subscribeSessQueues_ c True (tSess, qs')
|
|
forM_ (L.nonEmpty errs) $ notifySub c . ERRS . L.map (first qConnId)
|
|
|
|
subscribeSessQueues_ :: AgentClient -> Bool -> (SMPTransportSession, NonEmpty RcvQueueSub) -> AM' (BatchResponses RcvQueueSub AgentErrorType (Maybe ServiceId), Bool)
|
|
subscribeSessQueues_ c withEvents qs = sendClientBatch_ "SUB" False subscribe_ c NRMBackground qs
|
|
where
|
|
subscribe_ :: SMPClient -> NonEmpty RcvQueueSub -> IO (BatchResponses RcvQueueSub SMPClientError (Maybe ServiceId), Bool)
|
|
subscribe_ smp qs' = do
|
|
let (userId, srv, _) = tSess
|
|
atomically $ incSMPServerStat' c userId srv connSubAttempts $ length qs'
|
|
rs <- sendBatch (\smp' _ -> subscribeSMPQueues smp') smp NRMBackground qs'
|
|
cs_ <-
|
|
if withEvents
|
|
then Just . S.fromList . map qConnId . M.elems <$> atomically (SS.getActiveSubs tSess $ currentSubs c)
|
|
else pure Nothing
|
|
active <- E.uninterruptibleMask_ $ do
|
|
(active, (serviceQs, notices)) <- atomically $ do
|
|
r@(_, (_, notices)) <- ifM
|
|
(activeClientSession c tSess sessId)
|
|
((True,) <$> processSubResults c tSess sessId (smpClientServiceId smp) rs)
|
|
((False, ([], [])) <$ incSMPServerStat' c userId srv connSubIgnored (length rs))
|
|
unless (null notices) $ takeTMVar $ clientNoticesLock c
|
|
pure r
|
|
unless (null serviceQs) $ void $
|
|
processRcvServiceAssocs c serviceQs `runReaderT` agentEnv c
|
|
unless (null notices) $ void $
|
|
(processClientNotices c tSess notices `runReaderT` agentEnv c)
|
|
`E.finally` atomically (putTMVar (clientNoticesLock c) ())
|
|
pure active
|
|
forM_ cs_ $ \cs -> do
|
|
let (errs, okConns) = partitionEithers $ map (\(RcvQueueSub {connId}, r) -> bimap (connId,) (const connId) r) $ L.toList rs
|
|
conns = filter (`S.notMember` cs) okConns
|
|
unless (null conns) $ notifySub c $ UP srv conns
|
|
forM_ (L.nonEmpty errs) $ \errs' -> do
|
|
let noFinalErrs = all (temporaryClientError . snd) errs'
|
|
addr = B.unpack $ strEncode srv
|
|
notifySub c $ ERRS $ L.map (second $ protocolClientError SMP addr) errs'
|
|
when (null okConns && S.null cs && noFinalErrs && active) $ liftIO $ do
|
|
-- We only close the client session that was used to subscribe.
|
|
v_ <- atomically $ ifM (activeClientSession c tSess sessId) (TM.lookupDelete tSess $ smpClients c) (pure Nothing)
|
|
mapM_ (closeClient_ c) v_
|
|
pure (rs, active)
|
|
where
|
|
tSess = transportSession' smp
|
|
sessId = sessionId $ thParams smp
|
|
|
|
processRcvServiceAssocs :: SMPQueue q => AgentClient -> [q] -> AM' ()
|
|
processRcvServiceAssocs _ [] = pure ()
|
|
processRcvServiceAssocs c serviceQs =
|
|
withStore' c (`setRcvServiceAssocs` serviceQs) `catchAllErrors'` \e -> do
|
|
logError $ "processClientNotices error: " <> tshow e
|
|
notifySub' c "" $ ERR e
|
|
|
|
processClientNotices :: AgentClient -> SMPTransportSession -> [(RcvQueueSub, Maybe ClientNotice)] -> AM' ()
|
|
processClientNotices c@AgentClient {presetServers} tSess notices = do
|
|
now <- liftIO getSystemSeconds
|
|
tryAllErrors' (withStore' c $ \db -> (,) <$> updateClientNotices db tSess now notices <*> getClientNotices db presetServers) >>= \case
|
|
Right (noticeIds, clntNotices) -> atomically $ do
|
|
SS.updateClientNotices tSess noticeIds $ currentSubs c
|
|
writeTVar (clientNotices c) clntNotices
|
|
Left e -> do
|
|
logError $ "processClientNotices error: " <> tshow e
|
|
notifySub' c "" $ ERR e
|
|
|
|
resubscribeClientService :: AgentClient -> SMPTransportSession -> ServiceSub -> AM ServiceSubResult
|
|
resubscribeClientService c tSess@(userId, srv, _) serviceSub =
|
|
tryAllErrors (withServiceClient c tSess $ \smp _ -> subscribeClientService_ c True tSess smp serviceSub) >>= \case
|
|
Right r@(ServiceSubResult e _) -> case e of
|
|
Just SSErrorServiceId {} -> unassocSubscribeQueues $> r
|
|
_ -> pure r
|
|
Left e -> do
|
|
when (clientServiceError e) $ unassocSubscribeQueues
|
|
atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR e)
|
|
throwE e
|
|
where
|
|
unassocSubscribeQueues = do
|
|
qs <- withStore' c $ \db -> unassocUserServerRcvQueueSubs db userId srv
|
|
void $ lift $ subscribeUserServerQueues c userId srv qs
|
|
|
|
-- TODO [certs rcv] update service in the database if it has different ID and re-associate queues, and send event
|
|
subscribeClientService :: AgentClient -> Bool -> UserId -> SMPServer -> ServiceSub -> AM ServiceSubResult
|
|
subscribeClientService c withEvent userId srv (ServiceSub _ n idsHash) =
|
|
withServiceClient c tSess $ \smp smpServiceId -> do
|
|
let serviceSub = ServiceSub smpServiceId n idsHash
|
|
atomically $ SS.setPendingServiceSub tSess serviceSub $ currentSubs c
|
|
subscribeClientService_ c withEvent tSess smp serviceSub
|
|
where
|
|
tSess = (userId, srv, Nothing)
|
|
|
|
withServiceClient :: AgentClient -> SMPTransportSession -> (SMPClient -> ServiceId -> ExceptT SMPClientError IO a) -> AM a
|
|
withServiceClient c tSess subscribe =
|
|
withLogClient c NRMBackground tSess B.empty "SUBS" $ \(SMPConnectedClient smp _) ->
|
|
case smpClientServiceId smp of
|
|
Just smpServiceId -> subscribe smp smpServiceId
|
|
Nothing -> throwE PCEServiceUnavailable
|
|
|
|
-- TODO [certs rcv] send subscription error event?
|
|
subscribeClientService_ :: AgentClient -> Bool -> SMPTransportSession -> SMPClient -> ServiceSub -> ExceptT SMPClientError IO ServiceSubResult
|
|
subscribeClientService_ c withEvent tSess@(_, srv, _) smp expected@(ServiceSub _ n idsHash) = do
|
|
subscribed <- subscribeService smp SMP.SRecipientService n idsHash
|
|
let sessId = sessionId $ thParams smp
|
|
r = serviceSubResult expected subscribed
|
|
atomically $ whenM (activeClientSession c tSess sessId) $
|
|
SS.setActiveServiceSub tSess sessId subscribed $ currentSubs c
|
|
when withEvent $ notifySub c $ SERVICE_UP srv r
|
|
pure r
|
|
|
|
activeClientSession :: AgentClient -> SMPTransportSession -> SessionId -> STM Bool
|
|
activeClientSession c tSess sessId = sameSess <$> tryReadSessVar tSess (smpClients c)
|
|
where
|
|
sameSess = \case
|
|
Just (Right (SMPConnectedClient smp _)) -> sessId == sessionId (thParams smp)
|
|
_ -> False
|
|
|
|
type BatchResponses q e r = NonEmpty (q, Either e r)
|
|
|
|
-- Please note: this function does not preserve order of results to be the same as the order of arguments,
|
|
-- it includes arguments in the results instead.
|
|
sendTSessionBatches :: forall q r. ByteString -> (q -> TransportSessionMode -> SMPTransportSession) -> (SMPClient -> NonEmpty q -> IO (BatchResponses q SMPClientError r)) -> AgentClient -> NetworkRequestMode -> [q] -> AM' [(q, Either AgentErrorType r)]
|
|
sendTSessionBatches statCmd mkSession action c nm qs = do
|
|
qs' <- batchQueues mkSession qs <$> getSessionModeIO c
|
|
concatMap L.toList <$> mapConcurrently (sendClientBatch statCmd action c nm) qs'
|
|
|
|
batchQueues :: (q -> TransportSessionMode -> SMPTransportSession) -> [q] -> TransportSessionMode -> [(SMPTransportSession, NonEmpty q)]
|
|
batchQueues mkSession qs mode = M.assocs $ foldr batch M.empty qs
|
|
where
|
|
batch q m =
|
|
let tSess = mkSession q mode
|
|
in M.alter (Just . maybe [q] (q <|)) tSess m
|
|
|
|
sendClientBatch :: ByteString -> (SMPClient -> NonEmpty q -> IO (BatchResponses q SMPClientError r)) -> AgentClient -> NetworkRequestMode -> (SMPTransportSession, NonEmpty q) -> AM' (BatchResponses q AgentErrorType r)
|
|
sendClientBatch statCmd action = fmap fst .:. sendClientBatch_ statCmd () (fmap (,()) .: action)
|
|
{-# INLINE sendClientBatch #-}
|
|
|
|
sendClientBatch_ :: ByteString -> res -> (SMPClient -> NonEmpty q -> IO (BatchResponses q SMPClientError r, res)) -> AgentClient -> NetworkRequestMode -> (SMPTransportSession, NonEmpty q) -> AM' (BatchResponses q AgentErrorType r, res)
|
|
sendClientBatch_ statCmd errRes action c nm (tSess@(_, srv, _), qs') =
|
|
tryAllErrors' (getSMPServerClient c nm tSess) >>= \case
|
|
Left e -> pure (L.map (,Left e) qs', errRes)
|
|
Right (SMPConnectedClient smp _) -> liftIO $ do
|
|
logServer' "-->" c srv (bshow (length qs') <> " queues") statCmd
|
|
first (L.map agentError) <$> action smp qs'
|
|
where
|
|
agentError = second . first $ protocolClientError SMP $ B.unpack (strEncode srv)
|
|
|
|
sendBatch :: SomeRcvQueue q => (SMPClient -> NetworkRequestMode -> NonEmpty (SMP.RecipientId, SMP.RcvPrivateAuthKey) -> IO (NonEmpty (Either SMPClientError a))) -> SMPClient -> NetworkRequestMode -> NonEmpty q -> IO (BatchResponses q SMPClientError a)
|
|
sendBatch smpCmdFunc smp nm qs = L.zip qs <$> smpCmdFunc smp nm (L.map queueCreds qs)
|
|
where
|
|
queueCreds q = (queueId q, rcvAuthKey q)
|
|
|
|
failSubscription :: SomeRcvQueue q => AgentClient -> SMPTransportSession -> q -> SMPClientError -> STM ()
|
|
failSubscription c tSess rq e = do
|
|
let rId = queueId rq
|
|
TM.insert rId e =<< getRemovedSubs c (qUserId rq, qServer rq)
|
|
SS.deletePendingSub tSess rId $ currentSubs c
|
|
|
|
failSubscriptions :: AgentClient -> SMPTransportSession -> Map SMP.RecipientId SMPClientError -> STM ()
|
|
failSubscriptions c tSess@(uId, srv, _) qs = do
|
|
TM.union qs =<< getRemovedSubs c (uId, srv)
|
|
SS.batchDeletePendingSubs tSess (M.keysSet qs) $ currentSubs c
|
|
|
|
getRemovedSubs :: AgentClient -> (UserId, SMPServer) -> STM (TMap SMP.RecipientId SMPClientError)
|
|
getRemovedSubs AgentClient {removedSubs} k = TM.lookup k removedSubs >>= maybe new pure
|
|
where
|
|
new = do
|
|
s <- newTVar M.empty
|
|
TM.insert k s removedSubs
|
|
pure s
|
|
|
|
addNewQueueSubscription :: AgentClient -> RcvQueue -> SMPTransportSession -> SessionId -> Maybe ServiceId -> AM' ()
|
|
addNewQueueSubscription c rq' tSess sessId serviceId_ = do
|
|
let rq = rcvQueueSub rq'
|
|
same <- atomically $ do
|
|
modifyTVar' (subscrConns c) $ S.insert $ qConnId rq
|
|
active <- activeClientSession c tSess sessId
|
|
if active
|
|
then SS.addActiveSub tSess sessId serviceId_ rq' $ currentSubs c
|
|
else SS.addPendingSub tSess rq $ currentSubs c
|
|
pure active
|
|
unless same $ resubscribeSMPSession c tSess
|
|
|
|
hasActiveSubscription :: SomeRcvQueue q => AgentClient -> q -> STM Bool
|
|
hasActiveSubscription c rq = do
|
|
tSess <- mkSMPTransportSession c rq
|
|
SS.hasActiveSub tSess (queueId rq) $ currentSubs c
|
|
{-# INLINE hasActiveSubscription #-}
|
|
|
|
hasPendingSubscription :: SomeRcvQueue q => AgentClient -> q -> STM Bool
|
|
hasPendingSubscription c rq = do
|
|
tSess <- mkSMPTransportSession c rq
|
|
SS.hasPendingSub tSess (queueId rq) $ currentSubs c
|
|
{-# INLINE hasPendingSubscription #-}
|
|
|
|
hasRemovedSubscription :: SomeRcvQueue q => AgentClient -> q -> STM (Maybe SMPClientError)
|
|
hasRemovedSubscription c rq = do
|
|
TM.lookup (qUserId rq, qServer rq) (removedSubs c) $>>= TM.lookup (queueId rq)
|
|
|
|
removeSubscription :: SomeRcvQueue q => AgentClient -> SMPTransportSession -> ConnId -> q -> STM ()
|
|
removeSubscription c tSess connId rq = do
|
|
modifyTVar' (subscrConns c) $ S.delete connId
|
|
SS.deleteSub tSess (queueId rq) $ currentSubs c
|
|
|
|
removeSubscriptions :: SomeRcvQueue q => AgentClient -> [ConnId] -> [q] -> STM ()
|
|
removeSubscriptions c connIds qs = do
|
|
unless (null connIds) $ modifyTVar' (subscrConns c) (`S.difference` (S.fromList connIds))
|
|
qss <- batchQueues mkSMPTSession qs <$> getSessionMode c
|
|
forM_ qss $ \(tSess, qs') -> SS.batchDeleteSubs tSess (L.toList qs') $ currentSubs c
|
|
|
|
getSubscriptions :: AgentClient -> IO (Set ConnId)
|
|
getSubscriptions = readTVarIO . subscrConns
|
|
{-# INLINE getSubscriptions #-}
|
|
|
|
logServer :: MonadIO m => ByteString -> AgentClient -> ProtocolServer s -> EntityId -> ByteString -> m ()
|
|
logServer dir c srv = logServer' dir c srv . unEntityId
|
|
{-# INLINE logServer #-}
|
|
|
|
logServer' :: MonadIO m => ByteString -> AgentClient -> ProtocolServer s -> ByteString -> ByteString -> m ()
|
|
logServer' dir AgentClient {clientId} srv qStr cmdStr =
|
|
logInfo . decodeUtf8 $ B.unwords ["A", "(" <> bshow clientId <> ")", dir, showServer srv, ":", logSecret' qStr, cmdStr]
|
|
|
|
showServer :: ProtocolServer s -> ByteString
|
|
showServer ProtocolServer {host, port} =
|
|
strEncode host <> B.pack (if null port then "" else ':' : port)
|
|
{-# INLINE showServer #-}
|
|
|
|
logSecret :: EntityId -> ByteString
|
|
logSecret = logSecret' . unEntityId
|
|
{-# INLINE logSecret #-}
|
|
|
|
logSecret' :: ByteString -> ByteString
|
|
logSecret' = B64.encode . B.take 3
|
|
{-# INLINE logSecret' #-}
|
|
|
|
sendConfirmation :: AgentClient -> NetworkRequestMode -> SndQueue -> ByteString -> AM (Maybe SMPServer)
|
|
sendConfirmation c nm sq@SndQueue {userId, server, connId, sndId, queueMode, sndPrivateKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do
|
|
let (privHdr, spKey) = if senderCanSecure queueMode then (SMP.PHEmpty, Just sndPrivateKey) else (SMP.PHConfirmation (C.toPublic sndPrivateKey), Nothing)
|
|
clientMsg = SMP.ClientMessage privHdr agentConfirmation
|
|
msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg
|
|
sendOrProxySMPMessage c nm userId server connId "<CONF>" spKey sndId (MsgFlags {notification = True}) msg
|
|
sendConfirmation _ _ _ _ = throwE $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database"
|
|
|
|
sendInvitation :: AgentClient -> NetworkRequestMode -> UserId -> ConnId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer)
|
|
sendInvitation c nm userId connId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer, senderId, dhPublicKey})) (Compatible agentVersion) connReq connInfo = do
|
|
msg <- mkInvitation
|
|
sendOrProxySMPMessage c nm userId smpServer connId "<INV>" Nothing senderId (MsgFlags {notification = True}) msg
|
|
where
|
|
mkInvitation :: AM ByteString
|
|
-- this is only encrypted with per-queue E2E, not with double ratchet
|
|
mkInvitation = do
|
|
let agentEnvelope = AgentInvitation {agentVersion, connReq, connInfo}
|
|
agentCbEncryptOnce v dhPublicKey . smpEncode $
|
|
SMP.ClientMessage SMP.PHEmpty (smpEncode agentEnvelope)
|
|
|
|
getQueueMessage :: AgentClient -> RcvQueue -> AM (Maybe SMPMsgMeta)
|
|
getQueueMessage c rq@RcvQueue {server, rcvId, rcvPrivateKey} = do
|
|
atomically createTakeGetLock
|
|
msg_ <- withSMPClient c NRMInteractive rq "GET" $ \smp ->
|
|
getSMPMessage smp rcvPrivateKey rcvId
|
|
mapM decryptMeta msg_
|
|
where
|
|
decryptMeta msg@SMP.RcvMessage {msgId} = SMP.rcvMessageMeta msgId <$> decryptSMPMessage rq msg
|
|
createTakeGetLock = TM.alterF takeLock (server, rcvId) $ getMsgLocks c
|
|
where
|
|
takeLock l_ = do
|
|
l <- maybe (newTMVar ()) pure l_
|
|
takeTMVar l
|
|
pure $ Just l
|
|
{-# INLINE getQueueMessage #-}
|
|
|
|
decryptSMPMessage :: RcvQueue -> SMP.RcvMessage -> AM SMP.ClientRcvMsgBody
|
|
decryptSMPMessage rq SMP.RcvMessage {msgId, msgBody = SMP.EncRcvMsgBody body} =
|
|
liftEither $ parse SMP.clientRcvMsgBodyP (AGENT A_MESSAGE) =<< decrypt body
|
|
where
|
|
decrypt = agentCbDecrypt (rcvDhSecret rq) (C.cbNonce msgId)
|
|
|
|
secureQueue :: AgentClient -> NetworkRequestMode -> RcvQueue -> SndPublicAuthKey -> AM ()
|
|
secureQueue c nm rq@RcvQueue {rcvId, rcvPrivateKey} senderKey =
|
|
withSMPClient c nm rq "KEY <key>" $ \smp ->
|
|
secureSMPQueue smp nm rcvPrivateKey rcvId senderKey
|
|
|
|
secureSndQueue :: AgentClient -> NetworkRequestMode -> SndQueue -> AM ()
|
|
secureSndQueue c nm SndQueue {userId, connId, server, sndId, sndPrivateKey} =
|
|
void $ sendOrProxySMPCommand c nm userId server connId "SKEY <key>" sndId secureViaProxy secureDirectly
|
|
where
|
|
-- TODO track statistics
|
|
secureViaProxy smp proxySess = proxySecureSndSMPQueue smp nm proxySess sndPrivateKey sndId
|
|
secureDirectly smp = secureSndSMPQueue smp nm sndPrivateKey sndId
|
|
|
|
addQueueLink :: AgentClient -> NetworkRequestMode -> RcvQueue -> SMP.LinkId -> QueueLinkData -> AM ()
|
|
addQueueLink c nm rq@RcvQueue {rcvId, rcvPrivateKey} lnkId d =
|
|
withSMPClient c nm rq "LSET" $ \smp -> addSMPQueueLink smp nm rcvPrivateKey rcvId lnkId d
|
|
|
|
deleteQueueLink :: AgentClient -> NetworkRequestMode -> RcvQueue -> AM ()
|
|
deleteQueueLink c nm rq@RcvQueue {rcvId, rcvPrivateKey} =
|
|
withSMPClient c nm rq "LDEL" $ \smp -> deleteSMPQueueLink smp nm rcvPrivateKey rcvId
|
|
|
|
secureGetQueueLink :: AgentClient -> NetworkRequestMode -> UserId -> InvShortLink -> AM (SMP.SenderId, QueueLinkData)
|
|
secureGetQueueLink c nm userId InvShortLink {server, linkId, sndPrivateKey} =
|
|
snd <$> sendOrProxySMPCommand c nm userId server (unEntityId linkId) "LKEY <key>" linkId secureGetViaProxy secureGetDirectly
|
|
where
|
|
secureGetViaProxy smp proxySess = proxySecureGetSMPQueueLink smp nm proxySess sndPrivateKey linkId
|
|
secureGetDirectly smp = secureGetSMPQueueLink smp nm sndPrivateKey linkId
|
|
|
|
getQueueLink :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SMP.LinkId -> AM (SMP.SenderId, QueueLinkData)
|
|
getQueueLink c nm userId server lnkId =
|
|
snd <$> sendOrProxySMPCommand c nm userId server (unEntityId lnkId) "LGET" lnkId getViaProxy getDirectly
|
|
where
|
|
getViaProxy smp proxySess = proxyGetSMPQueueLink smp nm proxySess lnkId
|
|
getDirectly smp = getSMPQueueLink smp nm lnkId
|
|
|
|
enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey)
|
|
enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey =
|
|
withSMPClient c NRMBackground rq "NKEY <nkey>" $ \smp ->
|
|
enableSMPQueueNotifications smp rcvPrivateKey rcvId notifierKey rcvNtfPublicDhKey
|
|
|
|
data EnableQueueNtfReq = EnableQueueNtfReq
|
|
{ eqnrNtfSub :: NtfSubscription,
|
|
eqnrRq :: RcvQueue,
|
|
eqnrAuthKeyPair :: C.AAuthKeyPair,
|
|
eqnrRcvKeyPair :: C.KeyPairX25519
|
|
}
|
|
|
|
enableQueuesNtfs :: AgentClient -> [EnableQueueNtfReq] -> AM' [(EnableQueueNtfReq, Either AgentErrorType (SMP.NotifierId, SMP.RcvNtfPublicDhKey))]
|
|
enableQueuesNtfs c = sendTSessionBatches "NKEY" (mkSMPTSession . eqnrRq) enableQueues_ c NRMBackground
|
|
where
|
|
enableQueues_ :: SMPClient -> NonEmpty EnableQueueNtfReq -> IO (NonEmpty (EnableQueueNtfReq, Either (ProtocolClientError ErrorType) (SMP.NotifierId, RcvNtfPublicDhKey)))
|
|
enableQueues_ smp qs' = L.zip qs' <$> enableSMPQueuesNtfs smp (L.map queueCreds qs')
|
|
queueCreds :: EnableQueueNtfReq -> (SMP.RecipientId, SMP.RcvPrivateAuthKey, SMP.NtfPublicAuthKey, SMP.RcvNtfPublicDhKey)
|
|
queueCreds EnableQueueNtfReq {eqnrRq, eqnrAuthKeyPair, eqnrRcvKeyPair} =
|
|
let RcvQueue {rcvPrivateKey, rcvId} = eqnrRq
|
|
(ntfPublicKey, _) = eqnrAuthKeyPair
|
|
(rcvNtfPubDhKey, _) = eqnrRcvKeyPair
|
|
in (rcvId, rcvPrivateKey, ntfPublicKey, rcvNtfPubDhKey)
|
|
|
|
disableQueueNotifications :: AgentClient -> RcvQueue -> AM ()
|
|
disableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} =
|
|
withSMPClient c NRMBackground rq "NDEL" $ \smp ->
|
|
disableSMPQueueNotifications smp rcvPrivateKey rcvId
|
|
|
|
type DisableQueueNtfReq = (NtfSubscription, RcvQueue)
|
|
|
|
disableQueuesNtfs :: AgentClient -> [DisableQueueNtfReq] -> AM' [(DisableQueueNtfReq, Either AgentErrorType ())]
|
|
disableQueuesNtfs c = sendTSessionBatches "NDEL" (mkSMPTSession . snd) disableQueues_ c NRMBackground
|
|
where
|
|
disableQueues_ :: SMPClient -> NonEmpty DisableQueueNtfReq -> IO (NonEmpty (DisableQueueNtfReq, Either (ProtocolClientError ErrorType) ()))
|
|
disableQueues_ smp qs' = L.zip qs' <$> disableSMPQueuesNtfs smp (L.map queueCreds qs')
|
|
queueCreds :: DisableQueueNtfReq -> (SMP.RecipientId, SMP.RcvPrivateAuthKey)
|
|
queueCreds (_, RcvQueue {rcvPrivateKey, rcvId}) = (rcvId, rcvPrivateKey)
|
|
|
|
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM ()
|
|
sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId =
|
|
withSMPClient c NRMBackground rq ("ACK:" <> logSecret' msgId) $ \smp ->
|
|
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
|
|
|
hasGetLock :: SomeRcvQueue q => AgentClient -> q -> IO Bool
|
|
hasGetLock c rq =
|
|
TM.memberIO (qServer rq, queueId rq) $ getMsgLocks c
|
|
{-# INLINE hasGetLock #-}
|
|
|
|
releaseGetLock :: SomeRcvQueue q => AgentClient -> q -> STM ()
|
|
releaseGetLock c rq =
|
|
TM.lookup (qServer rq, queueId rq) (getMsgLocks c) >>= mapM_ (`tryPutTMVar` ())
|
|
{-# INLINE releaseGetLock #-}
|
|
|
|
releaseGetLocksIO :: SomeRcvQueue q => AgentClient -> [q] -> IO ()
|
|
releaseGetLocksIO _ [] = pure ()
|
|
releaseGetLocksIO c rqs = do
|
|
locks <- readTVarIO $ getMsgLocks c
|
|
forM_ rqs $ \rq ->
|
|
forM_ (M.lookup ((qServer rq, queueId rq)) locks) $ \lock ->
|
|
atomically $ tryPutTMVar lock ()
|
|
|
|
suspendQueue :: AgentClient -> NetworkRequestMode -> RcvQueue -> AM ()
|
|
suspendQueue c nm rq@RcvQueue {rcvId, rcvPrivateKey} =
|
|
withSMPClient c nm rq "OFF" $ \smp ->
|
|
suspendSMPQueue smp nm rcvPrivateKey rcvId
|
|
|
|
deleteQueue :: AgentClient -> NetworkRequestMode -> RcvQueue -> AM ()
|
|
deleteQueue c nm rq@RcvQueue {rcvId, rcvPrivateKey} = do
|
|
withSMPClient c nm rq "DEL" $ \smp ->
|
|
deleteSMPQueue smp nm rcvPrivateKey rcvId
|
|
|
|
deleteQueues :: AgentClient -> NetworkRequestMode -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())]
|
|
deleteQueues c nm = sendTSessionBatches "DEL" mkSMPTSession deleteQueues_ c nm
|
|
where
|
|
deleteQueues_ smp rqs = do
|
|
let (userId, srv, _) = transportSession' smp
|
|
atomically $ incSMPServerStat' c userId srv connDelAttempts $ length rqs
|
|
rs <- sendBatch deleteSMPQueues smp nm rqs
|
|
let successes = foldl' (\n (_, r) -> if isRight r then n + 1 else n) 0 rs
|
|
atomically $ incSMPServerStat' c userId srv connDeleted successes
|
|
pure rs
|
|
|
|
-- This is only used in background
|
|
sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer)
|
|
sendAgentMessage c sq@SndQueue {userId, server, connId, sndId, sndPrivateKey} msgFlags agentMsg = do
|
|
let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg
|
|
msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg
|
|
sendOrProxySMPMessage c NRMBackground userId server connId "<MSG>" (Just sndPrivateKey) sndId msgFlags msg
|
|
|
|
data ServerQueueInfo = ServerQueueInfo
|
|
{ server :: SMPServer,
|
|
rcvId :: Text,
|
|
sndId :: Text,
|
|
ntfId :: Maybe Text,
|
|
status :: Text,
|
|
info :: QueueInfo
|
|
}
|
|
deriving (Show)
|
|
|
|
getQueueInfo :: AgentClient -> NetworkRequestMode -> RcvQueue -> AM ServerQueueInfo
|
|
getQueueInfo c nm rq@RcvQueue {server, rcvId, rcvPrivateKey, sndId, status, clientNtfCreds} =
|
|
withSMPClient c nm rq "QUE" $ \smp -> do
|
|
info <- getSMPQueueInfo smp nm rcvPrivateKey rcvId
|
|
let ntfId = enc . (\ClientNtfCreds {notifierId} -> notifierId) <$> clientNtfCreds
|
|
pure ServerQueueInfo {server, rcvId = enc rcvId, sndId = enc sndId, ntfId, status = serializeQueueStatus status, info}
|
|
where
|
|
enc = decodeLatin1 . B64.encode . unEntityId
|
|
|
|
agentNtfRegisterToken :: AgentClient -> NetworkRequestMode -> NtfToken -> SMP.NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519)
|
|
agentNtfRegisterToken c nm NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey =
|
|
withClient c nm (0, ntfServer, Nothing) $ \ntf -> ntfRegisterToken ntf nm ntfPrivKey (NewNtfTkn deviceToken ntfPubKey pubDhKey)
|
|
|
|
agentNtfVerifyToken :: AgentClient -> NetworkRequestMode -> NtfTokenId -> NtfToken -> NtfRegCode -> AM ()
|
|
agentNtfVerifyToken c nm tknId NtfToken {ntfServer, ntfPrivKey} code =
|
|
withNtfClient c nm ntfServer tknId "TVFY" $ \ntf -> ntfVerifyToken ntf nm ntfPrivKey tknId code
|
|
|
|
agentNtfCheckToken :: AgentClient -> NetworkRequestMode -> NtfTokenId -> NtfToken -> AM NtfTknStatus
|
|
agentNtfCheckToken c nm tknId NtfToken {ntfServer, ntfPrivKey} =
|
|
withNtfClient c nm ntfServer tknId "TCHK" $ \ntf -> ntfCheckToken ntf nm ntfPrivKey tknId
|
|
|
|
agentNtfReplaceToken :: AgentClient -> NetworkRequestMode -> NtfTokenId -> NtfToken -> DeviceToken -> AM ()
|
|
agentNtfReplaceToken c nm tknId NtfToken {ntfServer, ntfPrivKey} token =
|
|
withNtfClient c nm ntfServer tknId "TRPL" $ \ntf -> ntfReplaceToken ntf nm ntfPrivKey tknId token
|
|
|
|
agentNtfDeleteToken :: AgentClient -> NetworkRequestMode -> NtfServer -> C.APrivateAuthKey -> NtfTokenId -> AM ()
|
|
agentNtfDeleteToken c nm ntfServer ntfPrivKey tknId =
|
|
withNtfClient c nm ntfServer tknId "TDEL" $ \ntf -> ntfDeleteToken ntf nm ntfPrivKey tknId
|
|
|
|
-- set to 0 to disable
|
|
agentNtfSetCronInterval :: AgentClient -> NetworkRequestMode -> NtfTokenId -> NtfToken -> Word16 -> AM ()
|
|
agentNtfSetCronInterval c nm tknId NtfToken {ntfServer, ntfPrivKey} interval =
|
|
withNtfClient c nm ntfServer tknId "TCRN" $ \ntf -> ntfSetCronInterval ntf nm ntfPrivKey tknId interval
|
|
|
|
agentNtfCreateSubscription :: AgentClient -> NtfTokenId -> NtfToken -> SMPQueueNtf -> SMP.NtfPrivateAuthKey -> AM NtfSubscriptionId
|
|
agentNtfCreateSubscription c tknId NtfToken {ntfServer, ntfPrivKey} smpQueue nKey =
|
|
withNtfClient c NRMBackground ntfServer tknId "SNEW" $ \ntf -> ntfCreateSubscription ntf ntfPrivKey (NewNtfSub tknId smpQueue nKey)
|
|
|
|
agentNtfCreateSubscriptions :: AgentClient -> NtfToken -> NonEmpty (NewNtfEntity 'Subscription) -> AM' (NonEmpty (Either AgentErrorType NtfSubscriptionId))
|
|
agentNtfCreateSubscriptions = withNtfBatch "SNEW" ntfCreateSubscriptions
|
|
|
|
agentNtfCheckSubscription :: AgentClient -> NtfToken -> NtfSubscriptionId -> AM NtfSubStatus
|
|
agentNtfCheckSubscription c NtfToken {ntfServer, ntfPrivKey} subId =
|
|
withNtfClient c NRMBackground ntfServer subId "SCHK" $ \ntf -> ntfCheckSubscription ntf ntfPrivKey subId
|
|
|
|
agentNtfCheckSubscriptions :: AgentClient -> NtfToken -> NonEmpty NtfSubscriptionId -> AM' (NonEmpty (Either AgentErrorType NtfSubStatus))
|
|
agentNtfCheckSubscriptions = withNtfBatch "SCHK" ntfCheckSubscriptions
|
|
|
|
-- This batch sends all commands to one ntf server (client can only use one server at a time)
|
|
withNtfBatch ::
|
|
ByteString ->
|
|
(NtfClient -> C.APrivateAuthKey -> NonEmpty a -> IO (NonEmpty (Either NtfClientError r))) ->
|
|
AgentClient ->
|
|
NtfToken ->
|
|
NonEmpty a ->
|
|
AM' (NonEmpty (Either AgentErrorType r))
|
|
withNtfBatch cmdStr action c NtfToken {ntfServer, ntfPrivKey} subs = do
|
|
let tSess = (0, ntfServer, Nothing)
|
|
tryAllErrors' (getNtfServerClient c NRMBackground tSess) >>= \case
|
|
Left e -> pure $ L.map (\_ -> Left e) subs
|
|
Right ntf -> liftIO $ do
|
|
logServer' "-->" c ntfServer (bshow (length subs) <> " subscriptions") cmdStr
|
|
L.map agentError <$> action ntf ntfPrivKey subs
|
|
where
|
|
agentError = first $ protocolClientError NTF $ B.unpack (strEncode ntfServer)
|
|
|
|
agentNtfDeleteSubscription :: AgentClient -> NtfSubscriptionId -> NtfToken -> AM ()
|
|
agentNtfDeleteSubscription c subId NtfToken {ntfServer, ntfPrivKey} =
|
|
withNtfClient c NRMBackground ntfServer subId "SDEL" $ \ntf -> ntfDeleteSubscription ntf ntfPrivKey subId
|
|
|
|
agentXFTPDownloadChunk :: AgentClient -> UserId -> FileDigest -> RcvFileChunkReplica -> XFTPRcvChunkSpec -> AM ()
|
|
agentXFTPDownloadChunk c userId (FileDigest chunkDigest) RcvFileChunkReplica {server, replicaId = ChunkReplicaId fId, replicaKey} chunkSpec = do
|
|
g <- asks random
|
|
withXFTPClient c (userId, server, chunkDigest) "FGET" $ \xftp -> X.downloadXFTPChunk g xftp replicaKey fId chunkSpec
|
|
|
|
agentXFTPNewChunk :: AgentClient -> SndFileChunk -> Int -> XFTPServerWithAuth -> AM NewSndChunkReplica
|
|
agentXFTPNewChunk c SndFileChunk {userId, chunkSpec = XFTPChunkSpec {chunkSize}, digest = FileDigest chunkDigest} n (ProtoServerWithAuth srv auth) = do
|
|
rKeys <- xftpRcvKeys n
|
|
(sndKey, replicaKey) <- atomically . C.generateAuthKeyPair C.SEd25519 =<< asks random
|
|
let fileInfo = FileInfo {sndKey, size = chunkSize, digest = chunkDigest}
|
|
logServer "-->" c srv NoEntity "FNEW"
|
|
tSess <- mkTransportSession c userId srv chunkDigest
|
|
(sndId, rIds) <- withClient c NRMBackground tSess $ \xftp -> X.createXFTPChunk xftp replicaKey fileInfo (L.map fst rKeys) auth
|
|
logServer "<--" c srv NoEntity $ B.unwords ["SIDS", logSecret sndId]
|
|
pure NewSndChunkReplica {server = srv, replicaId = ChunkReplicaId sndId, replicaKey, rcvIdsKeys = L.toList $ xftpRcvIdsKeys rIds rKeys}
|
|
|
|
agentXFTPUploadChunk :: AgentClient -> UserId -> FileDigest -> SndFileChunkReplica -> XFTPChunkSpec -> AM ()
|
|
agentXFTPUploadChunk c userId (FileDigest chunkDigest) SndFileChunkReplica {server, replicaId = ChunkReplicaId fId, replicaKey} chunkSpec =
|
|
withXFTPClient c (userId, server, chunkDigest) "FPUT" $ \xftp -> X.uploadXFTPChunk xftp replicaKey fId chunkSpec
|
|
|
|
agentXFTPAddRecipients :: AgentClient -> UserId -> FileDigest -> SndFileChunkReplica -> Int -> AM (NonEmpty (ChunkReplicaId, C.APrivateAuthKey))
|
|
agentXFTPAddRecipients c userId (FileDigest chunkDigest) SndFileChunkReplica {server, replicaId = ChunkReplicaId fId, replicaKey} n = do
|
|
rKeys <- xftpRcvKeys n
|
|
rIds <- withXFTPClient c (userId, server, chunkDigest) "FADD" $ \xftp -> X.addXFTPRecipients xftp replicaKey fId (L.map fst rKeys)
|
|
pure $ xftpRcvIdsKeys rIds rKeys
|
|
|
|
agentXFTPDeleteChunk :: AgentClient -> UserId -> DeletedSndChunkReplica -> AM ()
|
|
agentXFTPDeleteChunk c userId DeletedSndChunkReplica {server, replicaId = ChunkReplicaId fId, replicaKey, chunkDigest = FileDigest chunkDigest} =
|
|
withXFTPClient c (userId, server, chunkDigest) "FDEL" $ \xftp -> X.deleteXFTPChunk xftp replicaKey fId
|
|
|
|
xftpRcvKeys :: Int -> AM (NonEmpty C.AAuthKeyPair)
|
|
xftpRcvKeys n = do
|
|
rKeys <- atomically . replicateM n . C.generateAuthKeyPair C.SEd25519 =<< asks random
|
|
case L.nonEmpty rKeys of
|
|
Just rKeys' -> pure rKeys'
|
|
_ -> throwE $ INTERNAL "non-positive number of recipients"
|
|
|
|
xftpRcvIdsKeys :: NonEmpty EntityId -> NonEmpty C.AAuthKeyPair -> NonEmpty (ChunkReplicaId, C.APrivateAuthKey)
|
|
xftpRcvIdsKeys rIds rKeys = L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys
|
|
|
|
agentCbEncrypt :: SndQueue -> Maybe C.PublicKeyX25519 -> ByteString -> AM ByteString
|
|
agentCbEncrypt SndQueue {e2eDhSecret, smpClientVersion} e2ePubKey msg = do
|
|
cmNonce <- atomically . C.randomCbNonce =<< asks random
|
|
let paddedLen = maybe SMP.e2eEncMessageLength (const SMP.e2eEncConfirmationLength) e2ePubKey
|
|
cmEncBody <-
|
|
liftEither . first cryptoError $
|
|
C.cbEncrypt e2eDhSecret cmNonce msg paddedLen
|
|
let cmHeader = SMP.PubHeader smpClientVersion e2ePubKey
|
|
pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
|
|
|
|
-- add encoding as AgentInvitation'?
|
|
agentCbEncryptOnce :: VersionSMPC -> C.PublicKeyX25519 -> ByteString -> AM ByteString
|
|
agentCbEncryptOnce clientVersion dhRcvPubKey msg = do
|
|
g <- asks random
|
|
(dhSndPubKey, dhSndPrivKey) <- atomically $ C.generateKeyPair g
|
|
let e2eDhSecret = C.dh' dhRcvPubKey dhSndPrivKey
|
|
cmNonce <- atomically $ C.randomCbNonce g
|
|
cmEncBody <-
|
|
liftEither . first cryptoError $
|
|
C.cbEncrypt e2eDhSecret cmNonce msg SMP.e2eEncConfirmationLength
|
|
let cmHeader = SMP.PubHeader clientVersion (Just dhSndPubKey)
|
|
pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
|
|
|
|
-- | NaCl crypto-box decrypt - both for messages received from the server
|
|
-- and per-queue E2E encrypted messages from the sender that were inside.
|
|
agentCbDecrypt :: C.DhSecretX25519 -> C.CbNonce -> ByteString -> Either AgentErrorType ByteString
|
|
agentCbDecrypt dhSecret nonce msg =
|
|
first cryptoError $
|
|
C.cbDecrypt dhSecret nonce msg
|
|
|
|
cryptoError :: C.CryptoError -> AgentErrorType
|
|
cryptoError = \case
|
|
C.CryptoLargeMsgError -> CMD LARGE "CryptoLargeMsgError"
|
|
C.CryptoHeaderError _ -> AGENT A_MESSAGE -- parsing error
|
|
C.CERatchetDuplicateMessage -> AGENT A_DUPLICATE
|
|
C.AESDecryptError -> c DECRYPT_AES
|
|
C.CBDecryptError -> c DECRYPT_CB
|
|
C.CERatchetHeader -> c RATCHET_HEADER
|
|
C.CERatchetTooManySkipped n -> c $ RATCHET_SKIPPED n
|
|
C.CERatchetEarlierMessage n -> c $ RATCHET_EARLIER n
|
|
e -> INTERNAL $ show e
|
|
where
|
|
c = AGENT . A_CRYPTO
|
|
|
|
waitForWork :: MonadIO m => TMVar () -> m ()
|
|
waitForWork = void . atomically . readTMVar
|
|
{-# INLINE waitForWork #-}
|
|
|
|
withWork :: AgentClient -> TMVar () -> (DB.Connection -> IO (Either StoreError (Maybe a))) -> (a -> AM ()) -> AM ()
|
|
withWork c doWork = withWork_ c doWork . withStore' c
|
|
{-# INLINE withWork #-}
|
|
|
|
-- setting doWork flag to "no work" before getWork rather than after prevents race condition when flag is set to "has work" by another thread after getWork call.
|
|
withWork_ :: (AnyStoreError e', MonadIO m) => AgentClient -> TMVar () -> ExceptT e m (Either e' (Maybe a)) -> (a -> ExceptT e m ()) -> ExceptT e m ()
|
|
withWork_ c doWork getWork action =
|
|
noWork >> getWork >>= \case
|
|
Right (Just r) -> hasWork >> action r
|
|
Right Nothing -> pure ()
|
|
Left e
|
|
| isWorkItemError e -> notifyErr (CRITICAL False) e -- worker remains stopped here because the next iteration is likely to produce the same result
|
|
| otherwise -> hasWork >> notifyErr INTERNAL e
|
|
where
|
|
hasWork = atomically $ hasWorkToDo' doWork
|
|
noWork = liftIO $ noWorkToDo doWork
|
|
notifyErr err e = do
|
|
logError $ "withWork_ error: " <> tshow e
|
|
atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e)
|
|
|
|
withWorkItems :: (AnyStoreError e', MonadIO m) => AgentClient -> TMVar () -> ExceptT e m (Either e' [Either e' a]) -> (NonEmpty a -> ExceptT e m ()) -> ExceptT e m ()
|
|
withWorkItems c doWork getWork action = do
|
|
noWork >> getWork >>= \case
|
|
Right [] -> pure ()
|
|
Right rs -> do
|
|
let (errs, items) = partitionEithers rs
|
|
case L.nonEmpty items of
|
|
Just items' -> hasWork >> action items'
|
|
Nothing -> do
|
|
case find isWorkItemError errs of
|
|
Nothing -> hasWork
|
|
Just err -> do
|
|
notifyErr (CRITICAL False) err
|
|
unless (all isWorkItemError errs) hasWork
|
|
forM_ (L.nonEmpty errs) $ notifySub c . ERRS . L.map (\e -> ("", INTERNAL $ show e))
|
|
Left e
|
|
| isWorkItemError e -> notifyErr (CRITICAL False) e
|
|
| otherwise -> hasWork >> notifyErr INTERNAL e
|
|
where
|
|
hasWork = atomically $ hasWorkToDo' doWork
|
|
noWork = liftIO $ noWorkToDo doWork
|
|
notifyErr err e = do
|
|
logError $ "withWorkItems error: " <> tshow e
|
|
atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e)
|
|
|
|
noWorkToDo :: TMVar () -> IO ()
|
|
noWorkToDo = void . atomically . tryTakeTMVar
|
|
{-# INLINE noWorkToDo #-}
|
|
|
|
hasWorkToDo :: Worker -> STM ()
|
|
hasWorkToDo = hasWorkToDo' . doWork
|
|
{-# INLINE hasWorkToDo #-}
|
|
|
|
hasWorkToDo' :: TMVar () -> STM ()
|
|
hasWorkToDo' = void . (`tryPutTMVar` ())
|
|
{-# INLINE hasWorkToDo' #-}
|
|
|
|
endAgentOperation :: AgentClient -> AgentOperation -> STM ()
|
|
endAgentOperation c op = endOperation c op $ case op of
|
|
AONtfNetwork -> pure ()
|
|
AORcvNetwork ->
|
|
suspendOperation c AOMsgDelivery $
|
|
suspendSendingAndDatabase c
|
|
AOMsgDelivery ->
|
|
suspendSendingAndDatabase c
|
|
AOSndNetwork ->
|
|
suspendOperation c AODatabase $
|
|
notifySuspended c
|
|
AODatabase ->
|
|
notifySuspended c
|
|
|
|
suspendSendingAndDatabase :: AgentClient -> STM ()
|
|
suspendSendingAndDatabase c =
|
|
suspendOperation c AOSndNetwork $
|
|
suspendOperation c AODatabase $
|
|
notifySuspended c
|
|
|
|
suspendOperation :: AgentClient -> AgentOperation -> STM () -> STM ()
|
|
suspendOperation c op endedAction = do
|
|
n <- stateTVar (agentOpSel op c) $ \s -> (opsInProgress s, s {opSuspended = True})
|
|
-- unsafeIOToSTM $ putStrLn $ "suspendOperation_ " <> show op <> " " <> show n
|
|
when (n == 0) $ whenSuspending c endedAction
|
|
|
|
notifySuspended :: AgentClient -> STM ()
|
|
notifySuspended c = do
|
|
-- unsafeIOToSTM $ putStrLn "notifySuspended"
|
|
writeTBQueue (subQ c) ("", "", AEvt SAENone SUSPENDED)
|
|
writeTVar (agentState c) ASSuspended
|
|
|
|
endOperation :: AgentClient -> AgentOperation -> STM () -> STM ()
|
|
endOperation c op endedAction = do
|
|
(suspended, n) <- stateTVar (agentOpSel op c) $ \s ->
|
|
let n = max 0 (opsInProgress s - 1)
|
|
in ((opSuspended s, n), s {opsInProgress = n})
|
|
-- unsafeIOToSTM $ putStrLn $ "endOperation: " <> show op <> " " <> show suspended <> " " <> show n
|
|
when (suspended && n == 0) $ whenSuspending c endedAction
|
|
|
|
whenSuspending :: AgentClient -> STM () -> STM ()
|
|
whenSuspending c = whenM ((== ASSuspending) <$> readTVar (agentState c))
|
|
{-# INLINE whenSuspending #-}
|
|
|
|
beginAgentOperation :: AgentClient -> AgentOperation -> STM ()
|
|
beginAgentOperation c op = do
|
|
let opVar = agentOpSel op c
|
|
s <- readTVar opVar
|
|
-- unsafeIOToSTM $ putStrLn $ "beginOperation? " <> show op <> " " <> show (opsInProgress s)
|
|
when (opSuspended s) retry
|
|
-- unsafeIOToSTM $ putStrLn $ "beginOperation! " <> show op <> " " <> show (opsInProgress s + 1)
|
|
writeTVar opVar $! s {opsInProgress = opsInProgress s + 1}
|
|
|
|
agentOperationBracket :: MonadUnliftIO m => AgentClient -> AgentOperation -> (AgentClient -> IO ()) -> m a -> m a
|
|
agentOperationBracket c op check action =
|
|
E.bracket
|
|
(liftIO (check c) >> atomically (beginAgentOperation c op))
|
|
(\_ -> atomically $ endAgentOperation c op)
|
|
(const action)
|
|
|
|
waitUntilForeground :: AgentClient -> IO ()
|
|
waitUntilForeground c =
|
|
unlessM (foreground readTVarIO) $ atomically $ unlessM (foreground readTVar) retry
|
|
where
|
|
foreground :: Monad m => (TVar AgentState -> m AgentState) -> m Bool
|
|
foreground rd = (ASForeground ==) <$> rd (agentState c)
|
|
|
|
-- This function waits while agent is suspended, but will proceed while it is suspending,
|
|
-- to allow completing in-flight operations.
|
|
waitWhileSuspended :: AgentClient -> IO ()
|
|
waitWhileSuspended c =
|
|
whenM (suspended readTVarIO) $ atomically $ whenM (suspended readTVar) retry
|
|
where
|
|
suspended :: Monad m => (TVar AgentState -> m AgentState) -> m Bool
|
|
suspended rd = (ASSuspended ==) <$> rd (agentState c)
|
|
|
|
withStore' :: AgentClient -> (DB.Connection -> IO a) -> AM a
|
|
withStore' c action = withStore c $ fmap Right . action
|
|
{-# INLINE withStore' #-}
|
|
|
|
withStore :: AgentClient -> (DB.Connection -> IO (Either StoreError a)) -> AM a
|
|
withStore c action = do
|
|
st <- asks store
|
|
withExceptT storeError . ExceptT . liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $
|
|
withTransaction st action `E.catch` handleDBErrors
|
|
where
|
|
handleDBErrors :: E.SomeException -> IO (Either StoreError a)
|
|
handleDBErrors e = pure $ Left $ case E.fromException e of
|
|
Just (e' :: SQLError) ->
|
|
#if defined(dbPostgres)
|
|
SEInternal $ bshow e'
|
|
#else
|
|
let se = SQL.sqlError e'
|
|
busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked
|
|
in (if busy then SEDatabaseBusy else SEInternal) $ bshow e'
|
|
#endif
|
|
Nothing -> SEInternal $ bshow e
|
|
|
|
unsafeWithStore :: AgentClient -> (DB.Connection -> IO a) -> AM' a
|
|
unsafeWithStore c action = do
|
|
st <- asks store
|
|
liftIO $ agentOperationBracket c AODatabase (\_ -> pure ()) $ withTransaction st action
|
|
|
|
withStoreBatch :: Traversable t => AgentClient -> (DB.Connection -> t (IO (Either AgentErrorType a))) -> AM' (t (Either AgentErrorType a))
|
|
withStoreBatch c actions = do
|
|
st <- asks store
|
|
liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $
|
|
withTransaction st $
|
|
mapM (`E.catch` handleInternal) . actions
|
|
where
|
|
handleInternal :: E.SomeException -> IO (Either AgentErrorType a)
|
|
handleInternal = pure . Left . INTERNAL . show
|
|
|
|
withStoreBatch' :: Traversable t => AgentClient -> (DB.Connection -> t (IO a)) -> AM' (t (Either AgentErrorType a))
|
|
withStoreBatch' c actions = withStoreBatch c (fmap (fmap Right) . actions)
|
|
{-# INLINE withStoreBatch' #-}
|
|
|
|
storeError :: StoreError -> AgentErrorType
|
|
storeError = \case
|
|
SEConnNotFound -> CONN NOT_FOUND ""
|
|
SEUserNotFound -> NO_USER
|
|
SERatchetNotFound -> CONN NOT_FOUND ""
|
|
SEConnDuplicate -> CONN DUPLICATE ""
|
|
SEBadConnType cxt CRcv -> CONN SIMPLEX cxt
|
|
SEBadConnType cxt CSnd -> CONN SIMPLEX cxt
|
|
SEInvitationNotFound cxt invId -> CMD PROHIBITED $ "SEInvitationNotFound " <> cxt <> ", invitationId = " <> show invId
|
|
-- this error is never reported as store error,
|
|
-- it is used to wrap agent operations when "transaction-like" store access is needed
|
|
-- NOTE: network IO should NOT be used inside AgentStoreMonad
|
|
SEAgentError e -> e
|
|
SEDatabaseBusy e -> CRITICAL True $ B.unpack e
|
|
e -> INTERNAL $ show e
|
|
|
|
userServers :: forall p. (ProtocolTypeI p, UserProtocol p) => AgentClient -> TMap UserId (UserServers p)
|
|
userServers c = case protocolTypeI @p of
|
|
SPSMP -> smpServers c
|
|
SPXFTP -> xftpServers c
|
|
{-# INLINE userServers #-}
|
|
|
|
pickServer :: NonEmpty (Maybe OperatorId, ProtoServerWithAuth p) -> AM (ProtoServerWithAuth p)
|
|
pickServer = \case
|
|
(_, srv) :| [] -> pure srv
|
|
servers -> do
|
|
gen <- asks randomServer
|
|
atomically $ snd . (servers L.!!) <$> stateTVar gen (randomR (0, L.length servers - 1))
|
|
|
|
getNextServer ::
|
|
(ProtocolTypeI p, UserProtocol p) =>
|
|
AgentClient ->
|
|
UserId ->
|
|
(UserServers p -> NonEmpty (Maybe OperatorId, ProtoServerWithAuth p)) ->
|
|
[ProtocolServer p] ->
|
|
AM (ProtoServerWithAuth p)
|
|
getNextServer c userId srvsSel usedSrvs = do
|
|
srvs <- getUserServers_ c userId srvsSel
|
|
snd <$> getNextServer_ srvs (usedOperatorsHosts srvs usedSrvs)
|
|
|
|
usedOperatorsHosts :: NonEmpty (Maybe OperatorId, ProtoServerWithAuth p) -> [ProtocolServer p] -> (Set (Maybe OperatorId), Set TransportHost)
|
|
usedOperatorsHosts srvs usedSrvs = (usedOperators, usedHosts)
|
|
where
|
|
usedHosts = S.unions $ map serverHosts usedSrvs
|
|
usedOperators = S.fromList $ mapMaybe usedOp $ L.toList srvs
|
|
usedOp (op, srv) = if hasUsedHost srv then Just op else Nothing
|
|
hasUsedHost (ProtoServerWithAuth srv _) = any (`S.member` usedHosts) $ serverHosts srv
|
|
|
|
getNextServer_ ::
|
|
(ProtocolTypeI p, UserProtocol p) =>
|
|
NonEmpty (Maybe OperatorId, ProtoServerWithAuth p) ->
|
|
(Set (Maybe OperatorId), Set TransportHost) ->
|
|
AM (NonEmpty (Maybe OperatorId, ProtoServerWithAuth p), ProtoServerWithAuth p)
|
|
getNextServer_ servers (usedOperators, usedHosts) = do
|
|
-- choose from servers of unused operators, when possible
|
|
let otherOpsSrvs = filterOrAll ((`S.notMember` usedOperators) . fst) servers
|
|
-- choose from servers with unused hosts when possible
|
|
unusedSrvs = filterOrAll (isUnusedServer usedHosts) otherOpsSrvs
|
|
(otherOpsSrvs,) <$> pickServer unusedSrvs
|
|
where
|
|
filterOrAll p srvs = fromMaybe srvs $ L.nonEmpty $ L.filter p srvs
|
|
|
|
isUnusedServer :: Set TransportHost -> (Maybe OperatorId, ProtoServerWithAuth p) -> Bool
|
|
isUnusedServer usedHosts (_, ProtoServerWithAuth ProtocolServer {host} _) = all (`S.notMember` usedHosts) host
|
|
|
|
getUserServers_ ::
|
|
(ProtocolTypeI p, UserProtocol p) =>
|
|
AgentClient ->
|
|
UserId ->
|
|
(UserServers p -> NonEmpty (Maybe OperatorId, ProtoServerWithAuth p)) ->
|
|
AM (NonEmpty (Maybe OperatorId, ProtoServerWithAuth p))
|
|
getUserServers_ c userId srvsSel =
|
|
liftIO (TM.lookupIO userId $ userServers c) >>= \case
|
|
Just srvs -> pure $ srvsSel srvs
|
|
_ -> throwE $ INTERNAL "unknown userId - no user servers"
|
|
|
|
-- This function checks used servers and operators every time to allow
|
|
-- changing configuration while retry look is executing.
|
|
-- This function is not thread safe.
|
|
withNextSrv ::
|
|
(ProtocolTypeI p, UserProtocol p) =>
|
|
AgentClient ->
|
|
UserId ->
|
|
(UserServers p -> NonEmpty (Maybe OperatorId, ProtoServerWithAuth p)) ->
|
|
TVar (Set TransportHost) ->
|
|
[ProtocolServer p] ->
|
|
(ProtoServerWithAuth p -> AM a) ->
|
|
AM a
|
|
withNextSrv c userId srvsSel triedHosts usedSrvs action = do
|
|
srvs <- getUserServers_ c userId srvsSel
|
|
let (usedOperators, usedHosts) = usedOperatorsHosts srvs usedSrvs
|
|
tried <- readTVarIO triedHosts
|
|
let triedOrUsed = S.union tried usedHosts
|
|
(otherOpsSrvs, srvAuth@(ProtoServerWithAuth srv _)) <- getNextServer_ srvs (usedOperators, triedOrUsed)
|
|
let newHosts = serverHosts srv
|
|
unusedSrvs = L.filter (isUnusedServer $ S.union triedOrUsed newHosts) otherOpsSrvs
|
|
!tried' = if null unusedSrvs then S.empty else S.union tried newHosts
|
|
atomically $ writeTVar triedHosts tried'
|
|
action srvAuth
|
|
|
|
incSMPServerStat :: AgentClient -> UserId -> SMPServer -> (AgentSMPServerStats -> TVar Int) -> STM ()
|
|
incSMPServerStat c userId srv sel = incSMPServerStat' c userId srv sel 1
|
|
|
|
incSMPServerStat' :: AgentClient -> UserId -> SMPServer -> (AgentSMPServerStats -> TVar Int) -> Int -> STM ()
|
|
incSMPServerStat' = incServerStat (\AgentClient {smpServersStats = s} -> s) newAgentSMPServerStats
|
|
|
|
incXFTPServerStat :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int) -> STM ()
|
|
incXFTPServerStat c userId srv sel = incXFTPServerStat_ c userId srv sel 1
|
|
{-# INLINE incXFTPServerStat #-}
|
|
|
|
incXFTPServerStat' :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int) -> Int -> STM ()
|
|
incXFTPServerStat' = incXFTPServerStat_
|
|
{-# INLINE incXFTPServerStat' #-}
|
|
|
|
incXFTPServerSizeStat :: AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar Int64) -> Int64 -> STM ()
|
|
incXFTPServerSizeStat = incXFTPServerStat_
|
|
{-# INLINE incXFTPServerSizeStat #-}
|
|
|
|
incXFTPServerStat_ :: Num n => AgentClient -> UserId -> XFTPServer -> (AgentXFTPServerStats -> TVar n) -> n -> STM ()
|
|
incXFTPServerStat_ = incServerStat (\AgentClient {xftpServersStats = s} -> s) newAgentXFTPServerStats
|
|
{-# INLINE incXFTPServerStat_ #-}
|
|
|
|
incNtfServerStat :: AgentClient -> UserId -> NtfServer -> (AgentNtfServerStats -> TVar Int) -> STM ()
|
|
incNtfServerStat c userId srv sel = incNtfServerStat' c userId srv sel 1
|
|
{-# INLINE incNtfServerStat #-}
|
|
|
|
incNtfServerStat' :: AgentClient -> UserId -> NtfServer -> (AgentNtfServerStats -> TVar Int) -> Int -> STM ()
|
|
incNtfServerStat' = incServerStat (\AgentClient {ntfServersStats = s} -> s) newAgentNtfServerStats
|
|
{-# INLINE incNtfServerStat' #-}
|
|
|
|
incServerStat :: Num n => (AgentClient -> TMap (UserId, ProtocolServer p) s) -> STM s -> AgentClient -> UserId -> ProtocolServer p -> (s -> TVar n) -> n -> STM ()
|
|
incServerStat statsSel mkNewStats c userId srv sel n = do
|
|
TM.lookup (userId, srv) (statsSel c) >>= \case
|
|
Just v -> modifyTVar' (sel v) (+ n)
|
|
Nothing -> do
|
|
newStats <- mkNewStats
|
|
modifyTVar' (sel newStats) (+ n)
|
|
TM.insert (userId, srv) newStats (statsSel c)
|
|
|
|
data AgentServersSummary = AgentServersSummary
|
|
{ smpServersStats :: Map (UserId, SMPServer) AgentSMPServerStatsData,
|
|
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData,
|
|
ntfServersStats :: Map (UserId, NtfServer) AgentNtfServerStatsData,
|
|
statsStartedAt :: UTCTime,
|
|
smpServersSessions :: Map (UserId, SMPServer) ServerSessions,
|
|
smpServersSubs :: Map (UserId, SMPServer) SMPServerSubs,
|
|
xftpServersSessions :: Map (UserId, XFTPServer) ServerSessions,
|
|
xftpRcvInProgress :: [XFTPServer],
|
|
xftpSndInProgress :: [XFTPServer],
|
|
xftpDelInProgress :: [XFTPServer],
|
|
ntfServersSessions :: Map (UserId, NtfServer) ServerSessions
|
|
}
|
|
deriving (Show)
|
|
|
|
data SMPServerSubs = SMPServerSubs
|
|
{ ssActive :: Int, -- based on activeSubs
|
|
ssPending :: Int -- based on pendingSubs
|
|
}
|
|
deriving (Show)
|
|
|
|
data ServerSessions = ServerSessions
|
|
{ ssConnected :: Int,
|
|
ssErrors :: Int,
|
|
ssConnecting :: Int
|
|
}
|
|
deriving (Show)
|
|
|
|
getAgentSubsTotal :: AgentClient -> [UserId] -> IO (SMPServerSubs, Bool)
|
|
getAgentSubsTotal c userIds = do
|
|
(ssActive, ssPending) <- SS.foldSessionSubs addSub (0, 0) $ currentSubs c
|
|
sess <- hasSession . M.toList =<< readTVarIO (smpClients c)
|
|
pure (SMPServerSubs {ssActive, ssPending}, sess)
|
|
where
|
|
addSub :: (Int, Int) -> (SMPTransportSession, SS.SessSubs) -> IO (Int, Int)
|
|
addSub acc@(!ssActive, !ssPending) ((userId, _, _), s)
|
|
| userId `elem` userIds = do
|
|
(active, pending) <- SS.mapSubs M.size s
|
|
pure (ssActive + active, ssPending + pending)
|
|
| otherwise = pure acc
|
|
hasSession :: [(SMPTransportSession, SMPClientVar)] -> IO Bool
|
|
hasSession = \case
|
|
[] -> pure False
|
|
(s : ss) -> ifM (isConnected s) (pure True) (hasSession ss)
|
|
isConnected ((userId, _, _), SessionVar {sessionVar})
|
|
| userId `elem` userIds = atomically $ maybe False isRight <$> tryReadTMVar sessionVar
|
|
| otherwise = pure False
|
|
|
|
getAgentServersSummary :: AgentClient -> IO AgentServersSummary
|
|
getAgentServersSummary c@AgentClient {smpServersStats, xftpServersStats, ntfServersStats, srvStatsStartedAt, agentEnv} = do
|
|
sss <- mapM getAgentSMPServerStats =<< readTVarIO smpServersStats
|
|
xss <- mapM getAgentXFTPServerStats =<< readTVarIO xftpServersStats
|
|
nss <- mapM getAgentNtfServerStats =<< readTVarIO ntfServersStats
|
|
statsStartedAt <- readTVarIO srvStatsStartedAt
|
|
smpServersSessions <- countSessions =<< readTVarIO (smpClients c)
|
|
smpServersSubs <- getServerSubs
|
|
xftpServersSessions <- countSessions =<< readTVarIO (xftpClients c)
|
|
xftpRcvInProgress <- catMaybes <$> getXFTPWorkerSrvs xftpRcvWorkers
|
|
xftpSndInProgress <- catMaybes <$> getXFTPWorkerSrvs xftpSndWorkers
|
|
xftpDelInProgress <- getXFTPWorkerSrvs xftpDelWorkers
|
|
ntfServersSessions <- countSessions =<< readTVarIO (ntfClients c)
|
|
pure
|
|
AgentServersSummary
|
|
{ smpServersStats = sss,
|
|
xftpServersStats = xss,
|
|
ntfServersStats = nss,
|
|
statsStartedAt,
|
|
smpServersSessions,
|
|
smpServersSubs,
|
|
xftpServersSessions,
|
|
xftpRcvInProgress,
|
|
xftpSndInProgress,
|
|
xftpDelInProgress,
|
|
ntfServersSessions
|
|
}
|
|
where
|
|
getServerSubs = SS.foldSessionSubs addSub M.empty $ currentSubs c
|
|
where
|
|
addSub subs ((userId, srv, _), s) = do
|
|
(active, pending) <- SS.mapSubs M.size s
|
|
let add ss = ss {ssActive = ssActive ss + active, ssPending = ssPending ss + pending}
|
|
pure $ M.alter (Just . add . fromMaybe (SMPServerSubs 0 0)) (userId, srv) subs
|
|
Env {xftpAgent = XFTPAgent {xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}} = agentEnv
|
|
getXFTPWorkerSrvs workers = foldM addSrv [] . M.toList =<< readTVarIO workers
|
|
where
|
|
addSrv acc (srv, Worker {doWork}) = do
|
|
hasWork <- atomically $ not <$> isEmptyTMVar doWork
|
|
pure $ if hasWork then srv : acc else acc
|
|
countSessions :: Map (TransportSession msg) (ClientVar msg) -> IO (Map (UserId, ProtoServer msg) ServerSessions)
|
|
countSessions = foldM addClient M.empty . M.toList
|
|
where
|
|
addClient !acc ((userId, srv, _), SessionVar {sessionVar}) = do
|
|
c_ <- atomically $ tryReadTMVar sessionVar
|
|
pure $ M.alter (Just . add c_) (userId, srv) acc
|
|
where
|
|
add c_ = modifySessions c_ . fromMaybe ServerSessions {ssConnected = 0, ssErrors = 0, ssConnecting = 0}
|
|
modifySessions c_ ss = case c_ of
|
|
Just (Right _) -> ss {ssConnected = ssConnected ss + 1}
|
|
Just (Left _) -> ss {ssErrors = ssErrors ss + 1}
|
|
Nothing -> ss {ssConnecting = ssConnecting ss + 1}
|
|
|
|
data SubInfo = SubInfo {userId :: UserId, server :: Text, rcvId :: Text, subError :: Maybe String}
|
|
deriving (Show)
|
|
|
|
data SubscriptionsInfo = SubscriptionsInfo
|
|
{ activeSubscriptions :: [SubInfo],
|
|
pendingSubscriptions :: [SubInfo],
|
|
removedSubscriptions :: [SubInfo]
|
|
}
|
|
deriving (Show)
|
|
|
|
getAgentSubscriptions :: AgentClient -> IO SubscriptionsInfo
|
|
getAgentSubscriptions c = do
|
|
(activeSubscriptions, pendingSubscriptions) <- SS.foldSessionSubs addSubs ([], []) $ currentSubs c
|
|
removedSubscriptions <- getRemoved
|
|
pure $ SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions}
|
|
where
|
|
addSubs :: ([SubInfo], [SubInfo]) -> (SMPTransportSession, SS.SessSubs) -> IO ([SubInfo], [SubInfo])
|
|
addSubs (active, pending) ((userId, srv, _), s) = do
|
|
(active', pending') <- SS.mapSubs (map (\rId -> subInfo (userId, srv, rId) Nothing) . M.keys) s
|
|
pure (active' ++ active, pending' ++ pending)
|
|
getRemoved :: IO [SubInfo]
|
|
getRemoved = foldM addSubInfo [] . M.assocs =<< readTVarIO (removedSubs c)
|
|
where
|
|
addSubInfo :: [SubInfo] -> ((UserId, SMPServer), TMap SMP.RecipientId SMPClientError) -> IO [SubInfo]
|
|
addSubInfo ss ((uId, srv), errs) = do
|
|
ss' <- map (\(rId, e) -> subInfo (uId, srv, rId) (Just e)) . M.assocs <$> readTVarIO errs
|
|
pure $ ss' ++ ss
|
|
subInfo :: (UserId, SMPServer, SMP.RecipientId) -> Maybe SMPClientError -> SubInfo
|
|
subInfo (uId, srv, rId) err = SubInfo {userId = uId, server = enc srv, rcvId = enc rId, subError = show <$> err}
|
|
enc :: StrEncoding a => a -> Text
|
|
enc = decodeLatin1 . strEncode
|
|
|
|
data AgentWorkersDetails = AgentWorkersDetails
|
|
{ smpClients_ :: [Text],
|
|
ntfClients_ :: [Text],
|
|
xftpClients_ :: [Text],
|
|
smpDeliveryWorkers_ :: Map Text WorkersDetails,
|
|
asyncCmdWorkers_ :: Map Text WorkersDetails,
|
|
smpSubWorkers_ :: [Text],
|
|
ntfWorkers_ :: Map Text WorkersDetails,
|
|
ntfSMPWorkers_ :: Map Text WorkersDetails,
|
|
xftpRcvWorkers_ :: Map Text WorkersDetails,
|
|
xftpSndWorkers_ :: Map Text WorkersDetails,
|
|
xftpDelWorkers_ :: Map Text WorkersDetails
|
|
}
|
|
deriving (Show)
|
|
|
|
data WorkersDetails = WorkersDetails
|
|
{ restarts :: Int,
|
|
hasWork :: Bool,
|
|
hasAction :: Bool
|
|
}
|
|
deriving (Show)
|
|
|
|
getAgentWorkersDetails :: AgentClient -> IO AgentWorkersDetails
|
|
getAgentWorkersDetails AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, agentEnv} = do
|
|
smpClients_ <- textKeys <$> readTVarIO smpClients
|
|
ntfClients_ <- textKeys <$> readTVarIO ntfClients
|
|
xftpClients_ <- textKeys <$> readTVarIO xftpClients
|
|
smpDeliveryWorkers_ <- workerStats . fmap fst =<< readTVarIO smpDeliveryWorkers
|
|
asyncCmdWorkers_ <- workerStats =<< readTVarIO asyncCmdWorkers
|
|
smpSubWorkers_ <- textKeys <$> readTVarIO smpSubWorkers
|
|
ntfWorkers_ <- workerStats =<< readTVarIO ntfWorkers
|
|
ntfSMPWorkers_ <- workerStats =<< readTVarIO ntfSMPWorkers
|
|
xftpRcvWorkers_ <- workerStats =<< readTVarIO xftpRcvWorkers
|
|
xftpSndWorkers_ <- workerStats =<< readTVarIO xftpSndWorkers
|
|
xftpDelWorkers_ <- workerStats =<< readTVarIO xftpDelWorkers
|
|
pure
|
|
AgentWorkersDetails
|
|
{ smpClients_,
|
|
ntfClients_,
|
|
xftpClients_,
|
|
smpDeliveryWorkers_,
|
|
asyncCmdWorkers_,
|
|
smpSubWorkers_,
|
|
ntfWorkers_,
|
|
ntfSMPWorkers_,
|
|
xftpRcvWorkers_,
|
|
xftpSndWorkers_,
|
|
xftpDelWorkers_
|
|
}
|
|
where
|
|
textKeys :: StrEncoding k => Map k v -> [Text]
|
|
textKeys = map textKey . M.keys
|
|
textKey :: StrEncoding k => k -> Text
|
|
textKey = decodeASCII . strEncode
|
|
workerStats :: StrEncoding k => Map k Worker -> IO (Map Text WorkersDetails)
|
|
workerStats ws = fmap M.fromList . forM (M.toList ws) $ \(qa, Worker {restarts, doWork, action}) -> do
|
|
RestartCount {restartCount} <- readTVarIO restarts
|
|
hasWork <- atomically $ not <$> isEmptyTMVar doWork
|
|
hasAction <- atomically $ not <$> isEmptyTMVar action
|
|
pure (textKey qa, WorkersDetails {restarts = restartCount, hasWork, hasAction})
|
|
Env {ntfSupervisor, xftpAgent} = agentEnv
|
|
NtfSupervisor {ntfWorkers, ntfSMPWorkers} = ntfSupervisor
|
|
XFTPAgent {xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers} = xftpAgent
|
|
|
|
data AgentWorkersSummary = AgentWorkersSummary
|
|
{ smpClientsCount :: Int,
|
|
ntfClientsCount :: Int,
|
|
xftpClientsCount :: Int,
|
|
smpDeliveryWorkersCount :: WorkersSummary,
|
|
asyncCmdWorkersCount :: WorkersSummary,
|
|
smpSubWorkersCount :: Int,
|
|
ntfWorkersCount :: WorkersSummary,
|
|
ntfSMPWorkersCount :: WorkersSummary,
|
|
xftpRcvWorkersCount :: WorkersSummary,
|
|
xftpSndWorkersCount :: WorkersSummary,
|
|
xftpDelWorkersCount :: WorkersSummary
|
|
}
|
|
deriving (Show)
|
|
|
|
data WorkersSummary = WorkersSummary
|
|
{ numActive :: Int,
|
|
numIdle :: Int,
|
|
totalRestarts :: Int
|
|
}
|
|
deriving (Show)
|
|
|
|
getAgentWorkersSummary :: AgentClient -> IO AgentWorkersSummary
|
|
getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeliveryWorkers, asyncCmdWorkers, smpSubWorkers, agentEnv} = do
|
|
smpClientsCount <- M.size <$> readTVarIO smpClients
|
|
ntfClientsCount <- M.size <$> readTVarIO ntfClients
|
|
xftpClientsCount <- M.size <$> readTVarIO xftpClients
|
|
smpDeliveryWorkersCount <- readTVarIO smpDeliveryWorkers >>= workerSummary . fmap fst
|
|
asyncCmdWorkersCount <- readTVarIO asyncCmdWorkers >>= workerSummary
|
|
smpSubWorkersCount <- M.size <$> readTVarIO smpSubWorkers
|
|
ntfWorkersCount <- readTVarIO ntfWorkers >>= workerSummary
|
|
ntfSMPWorkersCount <- readTVarIO ntfSMPWorkers >>= workerSummary
|
|
xftpRcvWorkersCount <- readTVarIO xftpRcvWorkers >>= workerSummary
|
|
xftpSndWorkersCount <- readTVarIO xftpSndWorkers >>= workerSummary
|
|
xftpDelWorkersCount <- readTVarIO xftpDelWorkers >>= workerSummary
|
|
pure
|
|
AgentWorkersSummary
|
|
{ smpClientsCount,
|
|
ntfClientsCount,
|
|
xftpClientsCount,
|
|
smpDeliveryWorkersCount,
|
|
asyncCmdWorkersCount,
|
|
smpSubWorkersCount,
|
|
ntfWorkersCount,
|
|
ntfSMPWorkersCount,
|
|
xftpRcvWorkersCount,
|
|
xftpSndWorkersCount,
|
|
xftpDelWorkersCount
|
|
}
|
|
where
|
|
Env {ntfSupervisor, xftpAgent} = agentEnv
|
|
NtfSupervisor {ntfWorkers, ntfSMPWorkers} = ntfSupervisor
|
|
XFTPAgent {xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers} = xftpAgent
|
|
workerSummary :: M.Map k Worker -> IO WorkersSummary
|
|
workerSummary = liftIO . foldM byWork WorkersSummary {numActive = 0, numIdle = 0, totalRestarts = 0}
|
|
where
|
|
byWork WorkersSummary {numActive, numIdle, totalRestarts} Worker {action, restarts} = do
|
|
RestartCount {restartCount} <- readTVarIO restarts
|
|
ifM
|
|
(atomically $ isJust <$> tryReadTMVar action)
|
|
(pure WorkersSummary {numActive, numIdle = numIdle + 1, totalRestarts = totalRestarts + restartCount})
|
|
(pure WorkersSummary {numActive = numActive + 1, numIdle, totalRestarts = totalRestarts + restartCount})
|
|
|
|
data AgentQueuesInfo = AgentQueuesInfo
|
|
{ msgQInfo :: TBQueueInfo,
|
|
subQInfo :: TBQueueInfo,
|
|
smpClientsQueues :: Map Text (Int, UTCTime, ClientInfo)
|
|
}
|
|
deriving (Show)
|
|
|
|
data ClientInfo
|
|
= ClientInfoQueues {sndQInfo :: TBQueueInfo, rcvQInfo :: TBQueueInfo}
|
|
| ClientInfoError {clientError :: (AgentErrorType, Maybe UTCTime)}
|
|
| ClientInfoConnecting
|
|
deriving (Show)
|
|
|
|
getAgentQueuesInfo :: AgentClient -> IO AgentQueuesInfo
|
|
getAgentQueuesInfo AgentClient {msgQ, subQ, smpClients} = do
|
|
msgQInfo <- atomically $ getTBQueueInfo msgQ
|
|
subQInfo <- atomically $ getTBQueueInfo subQ
|
|
smpClientsMap <- readTVarIO smpClients
|
|
let smpClientsMap' = M.mapKeys (decodeLatin1 . strEncode) smpClientsMap
|
|
smpClientsQueues <- mapM getClientQueuesInfo smpClientsMap'
|
|
pure AgentQueuesInfo {msgQInfo, subQInfo, smpClientsQueues}
|
|
where
|
|
getClientQueuesInfo :: SMPClientVar -> IO (Int, UTCTime, ClientInfo)
|
|
getClientQueuesInfo SessionVar {sessionVar, sessionVarId, sessionVarTs} = do
|
|
clientInfo <-
|
|
atomically (tryReadTMVar sessionVar) >>= \case
|
|
Just (Right c) -> do
|
|
(sndQInfo, rcvQInfo) <- getProtocolClientQueuesInfo $ protocolClient c
|
|
pure ClientInfoQueues {sndQInfo, rcvQInfo}
|
|
Just (Left e) -> pure $ ClientInfoError e
|
|
Nothing -> pure ClientInfoConnecting
|
|
pure (sessionVarId, sessionVarTs, clientInfo)
|
|
|
|
$(J.deriveJSON defaultJSON ''AgentLocks)
|
|
|
|
$(J.deriveJSON (enumJSON $ dropPrefix "TS") ''ProtocolTestStep)
|
|
|
|
$(J.deriveJSON defaultJSON ''ProtocolTestFailure)
|
|
|
|
$(J.deriveJSON defaultJSON ''ServerSessions)
|
|
|
|
$(J.deriveJSON defaultJSON ''SMPServerSubs)
|
|
|
|
$(J.deriveJSON defaultJSON ''AgentServersSummary)
|
|
|
|
$(J.deriveJSON defaultJSON ''SubInfo)
|
|
|
|
$(J.deriveJSON defaultJSON ''SubscriptionsInfo)
|
|
|
|
$(J.deriveJSON defaultJSON ''WorkersDetails)
|
|
|
|
$(J.deriveJSON defaultJSON ''WorkersSummary)
|
|
|
|
$(J.deriveJSON defaultJSON {J.fieldLabelModifier = takeWhile (/= '_')} ''AgentWorkersDetails)
|
|
|
|
$(J.deriveJSON defaultJSON ''AgentWorkersSummary)
|
|
|
|
$(J.deriveJSON (sumTypeJSON $ dropPrefix "ClientInfo") ''ClientInfo)
|
|
|
|
$(J.deriveJSON defaultJSON ''AgentQueuesInfo)
|
|
|
|
$(J.deriveJSON (enumJSON $ dropPrefix "UN") ''UserNetworkType)
|
|
|
|
$(J.deriveJSON defaultJSON ''UserNetworkInfo)
|
|
|
|
$(J.deriveJSON defaultJSON ''ServerQueueInfo)
|