{-# 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