mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-06-06 15:31:55 +00:00
4b9ebbbab2
* move SMP server from agent commands NEW/JOIN to agent config * fix SMPServer parser * update agent protocol - server management * enable agent test * agent test with 2 servers * create reply queue on the configured server * choose random server * swap bind
461 lines
16 KiB
Haskell
461 lines
16 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
|
|
|
module Simplex.Messaging.Agent.Transmission where
|
|
|
|
import Control.Applicative (optional, (<|>))
|
|
import Control.Monad.IO.Class
|
|
import Data.Attoparsec.ByteString.Char8 (Parser)
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.ByteString.Base64
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.Functor (($>))
|
|
import Data.Int (Int64)
|
|
import Data.Kind (Type)
|
|
import Data.String (IsString (..))
|
|
import Data.Time.Clock (UTCTime)
|
|
import Data.Time.ISO8601
|
|
import Data.Type.Equality
|
|
import Data.Typeable ()
|
|
import GHC.Generics (Generic)
|
|
import Generic.Random (genericArbitraryU)
|
|
import Network.Socket
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Parsers
|
|
import Simplex.Messaging.Protocol
|
|
( CorrId (..),
|
|
Encoded,
|
|
ErrorType,
|
|
MsgBody,
|
|
MsgId,
|
|
SenderPublicKey,
|
|
)
|
|
import qualified Simplex.Messaging.Protocol as SMP
|
|
import Simplex.Messaging.Transport
|
|
import Simplex.Messaging.Util
|
|
import System.IO
|
|
import Test.QuickCheck (Arbitrary (..))
|
|
import Text.Read
|
|
import UnliftIO.Exception
|
|
|
|
type ARawTransmission = (ByteString, ByteString, ByteString)
|
|
|
|
type ATransmission p = (CorrId, ConnAlias, ACommand p)
|
|
|
|
type ATransmissionOrError p = (CorrId, ConnAlias, Either AgentErrorType (ACommand p))
|
|
|
|
data AParty = Agent | Client
|
|
deriving (Eq, Show)
|
|
|
|
data SAParty :: AParty -> Type where
|
|
SAgent :: SAParty Agent
|
|
SClient :: SAParty Client
|
|
|
|
deriving instance Show (SAParty p)
|
|
|
|
deriving instance Eq (SAParty p)
|
|
|
|
instance TestEquality SAParty where
|
|
testEquality SAgent SAgent = Just Refl
|
|
testEquality SClient SClient = Just Refl
|
|
testEquality _ _ = Nothing
|
|
|
|
data ACmd = forall p. ACmd (SAParty p) (ACommand p)
|
|
|
|
deriving instance Show ACmd
|
|
|
|
data ACommand (p :: AParty) where
|
|
NEW :: ACommand Client -- response INV
|
|
INV :: SMPQueueInfo -> ACommand Agent
|
|
JOIN :: SMPQueueInfo -> ReplyMode -> ACommand Client -- response OK
|
|
CON :: ACommand Agent -- notification that connection is established
|
|
-- TODO currently it automatically allows whoever sends the confirmation
|
|
-- CONF :: OtherPartyId -> ACommand Agent
|
|
-- LET :: OtherPartyId -> ACommand Client
|
|
SUB :: ACommand Client
|
|
SUBALL :: ACommand Client -- TODO should be moved to chat protocol - hack for subscribing to all
|
|
END :: ACommand Agent
|
|
-- QST :: QueueDirection -> ACommand Client
|
|
-- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent
|
|
SEND :: MsgBody -> ACommand Client
|
|
SENT :: AgentMsgId -> ACommand Agent
|
|
MSG ::
|
|
{ recipientMeta :: (AgentMsgId, UTCTime),
|
|
brokerMeta :: (MsgId, UTCTime),
|
|
senderMeta :: (AgentMsgId, UTCTime),
|
|
msgIntegrity :: MsgIntegrity,
|
|
msgBody :: MsgBody
|
|
} ->
|
|
ACommand Agent
|
|
-- ACK :: AgentMsgId -> ACommand Client
|
|
-- RCVD :: AgentMsgId -> ACommand Agent
|
|
OFF :: ACommand Client
|
|
DEL :: ACommand Client
|
|
OK :: ACommand Agent
|
|
ERR :: AgentErrorType -> ACommand Agent
|
|
|
|
deriving instance Eq (ACommand p)
|
|
|
|
deriving instance Show (ACommand p)
|
|
|
|
type Message = ByteString
|
|
|
|
data SMPMessage
|
|
= SMPConfirmation SenderPublicKey
|
|
| SMPMessage
|
|
{ senderMsgId :: AgentMsgId,
|
|
senderTimestamp :: SenderTimestamp,
|
|
previousMsgHash :: ByteString,
|
|
agentMessage :: AMessage
|
|
}
|
|
deriving (Show)
|
|
|
|
data AMessage where
|
|
HELLO :: VerificationKey -> AckMode -> AMessage
|
|
REPLY :: SMPQueueInfo -> AMessage
|
|
A_MSG :: MsgBody -> AMessage
|
|
deriving (Show)
|
|
|
|
parseSMPMessage :: ByteString -> Either AgentErrorType SMPMessage
|
|
parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE
|
|
where
|
|
smpMessageP :: Parser SMPMessage
|
|
smpMessageP =
|
|
smpConfirmationP <* A.endOfLine
|
|
<|> A.endOfLine *> smpClientMessageP
|
|
|
|
smpConfirmationP :: Parser SMPMessage
|
|
smpConfirmationP = SMPConfirmation <$> ("KEY " *> C.pubKeyP <* A.endOfLine)
|
|
|
|
smpClientMessageP :: Parser SMPMessage
|
|
smpClientMessageP =
|
|
SMPMessage
|
|
<$> A.decimal <* A.space
|
|
<*> tsISO8601P <* A.space
|
|
-- TODO previous message hash should become mandatory when we support HELLO and REPLY
|
|
-- (for HELLO it would be the hash of SMPConfirmation)
|
|
<*> (base64P <|> pure "") <* A.endOfLine
|
|
<*> agentMessageP
|
|
|
|
serializeSMPMessage :: SMPMessage -> ByteString
|
|
serializeSMPMessage = \case
|
|
SMPConfirmation sKey -> smpMessage ("KEY " <> C.serializePubKey sKey) "" ""
|
|
SMPMessage {senderMsgId, senderTimestamp, previousMsgHash, agentMessage} ->
|
|
let header = messageHeader senderMsgId senderTimestamp previousMsgHash
|
|
body = serializeAgentMessage agentMessage
|
|
in smpMessage "" header body
|
|
where
|
|
messageHeader msgId ts prevMsgHash =
|
|
B.unwords [bshow msgId, B.pack $ formatISO8601Millis ts, encode prevMsgHash]
|
|
smpMessage smpHeader aHeader aBody = B.intercalate "\n" [smpHeader, aHeader, aBody, ""]
|
|
|
|
agentMessageP :: Parser AMessage
|
|
agentMessageP =
|
|
"HELLO " *> hello
|
|
<|> "REPLY " *> reply
|
|
<|> "MSG " *> a_msg
|
|
where
|
|
hello = HELLO <$> C.pubKeyP <*> ackMode
|
|
reply = REPLY <$> smpQueueInfoP
|
|
a_msg = do
|
|
size :: Int <- A.decimal <* A.endOfLine
|
|
A_MSG <$> A.take size <* A.endOfLine
|
|
ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On)
|
|
|
|
smpQueueInfoP :: Parser SMPQueueInfo
|
|
smpQueueInfoP =
|
|
"smp::" *> (SMPQueueInfo <$> smpServerP <* "::" <*> base64P <* "::" <*> C.pubKeyP)
|
|
|
|
smpServerP :: Parser SMPServer
|
|
smpServerP = SMPServer <$> server <*> optional port <*> optional kHash
|
|
where
|
|
server = B.unpack <$> A.takeWhile1 (A.notInClass ":# ")
|
|
port = A.char ':' *> (B.unpack <$> A.takeWhile1 A.isDigit)
|
|
kHash = C.KeyHash <$> (A.char '#' *> base64P)
|
|
|
|
parseAgentMessage :: ByteString -> Either AgentErrorType AMessage
|
|
parseAgentMessage = parse agentMessageP $ AGENT A_MESSAGE
|
|
|
|
serializeAgentMessage :: AMessage -> ByteString
|
|
serializeAgentMessage = \case
|
|
HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else ""
|
|
REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo
|
|
A_MSG body -> "MSG " <> serializeMsg body <> "\n"
|
|
|
|
serializeSmpQueueInfo :: SMPQueueInfo -> ByteString
|
|
serializeSmpQueueInfo (SMPQueueInfo srv qId ek) =
|
|
B.intercalate "::" ["smp", serializeServer srv, encode qId, C.serializePubKey ek]
|
|
|
|
serializeServer :: SMPServer -> ByteString
|
|
serializeServer SMPServer {host, port, keyHash} =
|
|
B.pack $ host <> maybe "" (':' :) port <> maybe "" (('#' :) . B.unpack . encode . C.unKeyHash) keyHash
|
|
|
|
data SMPServer = SMPServer
|
|
{ host :: HostName,
|
|
port :: Maybe ServiceName,
|
|
keyHash :: Maybe C.KeyHash
|
|
}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance IsString SMPServer where
|
|
fromString = parseString . parseAll $ smpServerP
|
|
|
|
type ConnAlias = ByteString
|
|
|
|
type OtherPartyId = Encoded
|
|
|
|
data OnOff = On | Off deriving (Eq, Show, Read)
|
|
|
|
newtype AckMode = AckMode OnOff deriving (Eq, Show)
|
|
|
|
data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey
|
|
deriving (Eq, Show)
|
|
|
|
newtype ReplyMode = ReplyMode OnOff deriving (Eq, Show)
|
|
|
|
type EncryptionKey = C.PublicKey
|
|
|
|
type DecryptionKey = C.SafePrivateKey
|
|
|
|
type SignatureKey = C.SafePrivateKey
|
|
|
|
type VerificationKey = C.PublicKey
|
|
|
|
data QueueDirection = SND | RCV deriving (Show)
|
|
|
|
data QueueStatus = New | Confirmed | Secured | Active | Disabled
|
|
deriving (Eq, Show, Read)
|
|
|
|
type AgentMsgId = Int64
|
|
|
|
type SenderTimestamp = UTCTime
|
|
|
|
data MsgIntegrity = MsgOk | MsgError MsgErrorType
|
|
deriving (Eq, Show)
|
|
|
|
data MsgErrorType = MsgSkipped AgentMsgId AgentMsgId | MsgBadId AgentMsgId | MsgBadHash | MsgDuplicate
|
|
deriving (Eq, Show)
|
|
|
|
-- | error type used in errors sent to agent clients
|
|
data AgentErrorType
|
|
= CMD CommandErrorType -- command errors
|
|
| CONN ConnectionErrorType -- connection state errors
|
|
| SMP ErrorType -- SMP protocol errors forwarded to agent clients
|
|
| BROKER BrokerErrorType -- SMP server errors
|
|
| AGENT SMPAgentError -- errors of other agents
|
|
| INTERNAL String -- agent implementation errors
|
|
deriving (Eq, Generic, Read, Show, Exception)
|
|
|
|
data CommandErrorType
|
|
= PROHIBITED -- command is prohibited
|
|
| SYNTAX -- command syntax is invalid
|
|
| NO_CONN -- connection alias is required with this command
|
|
| SIZE -- message size is not correct (no terminating space)
|
|
| LARGE -- message does not fit SMP block
|
|
deriving (Eq, Generic, Read, Show, Exception)
|
|
|
|
data ConnectionErrorType
|
|
= UNKNOWN -- connection alias not in database
|
|
| DUPLICATE -- connection alias already exists
|
|
| SIMPLEX -- connection is simplex, but operation requires another queue
|
|
deriving (Eq, Generic, Read, Show, Exception)
|
|
|
|
data BrokerErrorType
|
|
= RESPONSE ErrorType -- invalid server response (failed to parse)
|
|
| UNEXPECTED -- unexpected response
|
|
| NETWORK -- network error
|
|
| TRANSPORT TransportError -- handshake or other transport error
|
|
| TIMEOUT -- command response timeout
|
|
deriving (Eq, Generic, Read, Show, Exception)
|
|
|
|
data SMPAgentError
|
|
= A_MESSAGE -- possibly should include bytestring that failed to parse
|
|
| A_PROHIBITED -- possibly should include the prohibited SMP/agent message
|
|
| A_ENCRYPTION -- cannot RSA/AES-decrypt or parse decrypted header
|
|
| A_SIGNATURE -- invalid RSA signature
|
|
deriving (Eq, Generic, Read, Show, Exception)
|
|
|
|
instance Arbitrary AgentErrorType where arbitrary = genericArbitraryU
|
|
|
|
instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU
|
|
|
|
instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU
|
|
|
|
instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU
|
|
|
|
instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU
|
|
|
|
commandP :: Parser ACmd
|
|
commandP =
|
|
"NEW" $> ACmd SClient NEW
|
|
<|> "INV " *> invResp
|
|
<|> "JOIN " *> joinCmd
|
|
<|> "SUB" $> ACmd SClient SUB
|
|
<|> "SUBALL" $> ACmd SClient SUBALL -- TODO remove - hack for subscribing to all
|
|
<|> "END" $> ACmd SAgent END
|
|
<|> "SEND " *> sendCmd
|
|
<|> "SENT " *> sentResp
|
|
<|> "MSG " *> message
|
|
<|> "OFF" $> ACmd SClient OFF
|
|
<|> "DEL" $> ACmd SClient DEL
|
|
<|> "ERR " *> agentError
|
|
<|> "CON" $> ACmd SAgent CON
|
|
<|> "OK" $> ACmd SAgent OK
|
|
where
|
|
invResp = ACmd SAgent . INV <$> smpQueueInfoP
|
|
joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <*> replyMode)
|
|
sendCmd = ACmd SClient . SEND <$> A.takeByteString
|
|
sentResp = ACmd SAgent . SENT <$> A.decimal
|
|
message = do
|
|
msgIntegrity <- msgIntegrityP <* A.space
|
|
recipientMeta <- "R=" *> partyMeta A.decimal
|
|
brokerMeta <- "B=" *> partyMeta base64P
|
|
senderMeta <- "S=" *> partyMeta A.decimal
|
|
msgBody <- A.takeByteString
|
|
return $ ACmd SAgent MSG {recipientMeta, brokerMeta, senderMeta, msgIntegrity, msgBody}
|
|
replyMode = ReplyMode <$> (" NO_REPLY" $> Off <|> pure On)
|
|
partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P <* A.space
|
|
agentError = ACmd SAgent . ERR <$> agentErrorTypeP
|
|
|
|
msgIntegrityP :: Parser MsgIntegrity
|
|
msgIntegrityP = "OK" $> MsgOk <|> "ERR " *> (MsgError <$> msgErrorType)
|
|
where
|
|
msgErrorType =
|
|
"ID " *> (MsgBadId <$> A.decimal)
|
|
<|> "IDS " *> (MsgSkipped <$> A.decimal <* A.space <*> A.decimal)
|
|
<|> "HASH" $> MsgBadHash
|
|
<|> "DUPLICATE" $> MsgDuplicate
|
|
|
|
parseCommand :: ByteString -> Either AgentErrorType ACmd
|
|
parseCommand = parse commandP $ CMD SYNTAX
|
|
|
|
serializeCommand :: ACommand p -> ByteString
|
|
serializeCommand = \case
|
|
NEW -> "NEW"
|
|
INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo
|
|
JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode
|
|
SUB -> "SUB"
|
|
SUBALL -> "SUBALL" -- TODO remove - hack for subscribing to all
|
|
END -> "END"
|
|
SEND msgBody -> "SEND " <> serializeMsg msgBody
|
|
SENT mId -> "SENT " <> bshow mId
|
|
MSG {recipientMeta = (rmId, rTs), brokerMeta = (bmId, bTs), senderMeta = (smId, sTs), msgIntegrity, msgBody} ->
|
|
B.unwords
|
|
[ "MSG",
|
|
serializeMsgIntegrity msgIntegrity,
|
|
"R=" <> bshow rmId <> "," <> showTs rTs,
|
|
"B=" <> encode bmId <> "," <> showTs bTs,
|
|
"S=" <> bshow smId <> "," <> showTs sTs,
|
|
serializeMsg msgBody
|
|
]
|
|
OFF -> "OFF"
|
|
DEL -> "DEL"
|
|
CON -> "CON"
|
|
ERR e -> "ERR " <> serializeAgentError e
|
|
OK -> "OK"
|
|
where
|
|
replyMode :: ReplyMode -> ByteString
|
|
replyMode = \case
|
|
ReplyMode Off -> " NO_REPLY"
|
|
ReplyMode On -> ""
|
|
showTs :: UTCTime -> ByteString
|
|
showTs = B.pack . formatISO8601Millis
|
|
|
|
serializeMsgIntegrity :: MsgIntegrity -> ByteString
|
|
serializeMsgIntegrity = \case
|
|
MsgOk -> "OK"
|
|
MsgError e ->
|
|
"ERR " <> case e of
|
|
MsgSkipped fromMsgId toMsgId ->
|
|
B.unwords ["NO_ID", bshow fromMsgId, bshow toMsgId]
|
|
MsgBadId aMsgId -> "ID " <> bshow aMsgId
|
|
MsgBadHash -> "HASH"
|
|
MsgDuplicate -> "DUPLICATE"
|
|
|
|
agentErrorTypeP :: Parser AgentErrorType
|
|
agentErrorTypeP =
|
|
"SMP " *> (SMP <$> SMP.errorTypeP)
|
|
<|> "BROKER RESPONSE " *> (BROKER . RESPONSE <$> SMP.errorTypeP)
|
|
<|> "BROKER TRANSPORT " *> (BROKER . TRANSPORT <$> transportErrorP)
|
|
<|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString)
|
|
<|> parseRead2
|
|
|
|
serializeAgentError :: AgentErrorType -> ByteString
|
|
serializeAgentError = \case
|
|
SMP e -> "SMP " <> SMP.serializeErrorType e
|
|
BROKER (RESPONSE e) -> "BROKER RESPONSE " <> SMP.serializeErrorType e
|
|
BROKER (TRANSPORT e) -> "BROKER TRANSPORT " <> serializeTransportError e
|
|
e -> bshow e
|
|
|
|
serializeMsg :: ByteString -> ByteString
|
|
serializeMsg body = bshow (B.length body) <> "\n" <> body
|
|
|
|
tPutRaw :: Handle -> ARawTransmission -> IO ()
|
|
tPutRaw h (corrId, connAlias, command) = do
|
|
putLn h corrId
|
|
putLn h connAlias
|
|
putLn h command
|
|
|
|
tGetRaw :: Handle -> IO ARawTransmission
|
|
tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h
|
|
|
|
tPut :: MonadIO m => Handle -> ATransmission p -> m ()
|
|
tPut h (CorrId corrId, connAlias, command) =
|
|
liftIO $ tPutRaw h (corrId, connAlias, serializeCommand command)
|
|
|
|
-- | get client and agent transmissions
|
|
tGet :: forall m p. MonadIO m => SAParty p -> Handle -> m (ATransmissionOrError p)
|
|
tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
|
where
|
|
tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)
|
|
tParseLoadBody t@(corrId, connAlias, command) = do
|
|
let cmd = parseCommand command >>= fromParty >>= tConnAlias t
|
|
fullCmd <- either (return . Left) cmdWithMsgBody cmd
|
|
return (CorrId corrId, connAlias, fullCmd)
|
|
|
|
fromParty :: ACmd -> Either AgentErrorType (ACommand p)
|
|
fromParty (ACmd (p :: p1) cmd) = case testEquality party p of
|
|
Just Refl -> Right cmd
|
|
_ -> Left $ CMD PROHIBITED
|
|
|
|
tConnAlias :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p)
|
|
tConnAlias (_, connAlias, _) cmd = case cmd of
|
|
-- NEW and JOIN have optional connAlias
|
|
NEW -> Right cmd
|
|
JOIN _ _ -> Right cmd
|
|
-- ERROR response does not always have connAlias
|
|
ERR _ -> Right cmd
|
|
-- other responses must have connAlias
|
|
_
|
|
| B.null connAlias -> Left $ CMD NO_CONN
|
|
| otherwise -> Right cmd
|
|
|
|
cmdWithMsgBody :: ACommand p -> m (Either AgentErrorType (ACommand p))
|
|
cmdWithMsgBody = \case
|
|
SEND body -> SEND <$$> getMsgBody body
|
|
MSG agentMsgId srvTS agentTS integrity body -> MSG agentMsgId srvTS agentTS integrity <$$> getMsgBody body
|
|
cmd -> return $ Right cmd
|
|
|
|
-- TODO refactor with server
|
|
getMsgBody :: MsgBody -> m (Either AgentErrorType MsgBody)
|
|
getMsgBody msgBody =
|
|
case B.unpack msgBody of
|
|
':' : body -> return . Right $ B.pack body
|
|
str -> case readMaybe str :: Maybe Int of
|
|
Just size -> liftIO $ do
|
|
body <- B.hGet h size
|
|
s <- getLn h
|
|
return $ if B.null s then Right body else Left $ CMD SIZE
|
|
Nothing -> return . Left $ CMD SYNTAX
|