This repository has been archived on 2022-09-21. You can view files and clone it, but cannot push or open issues or pull requests.
simplexmq/src/Simplex/Messaging/Agent/Transmission.hs

383 lines
12 KiB
Haskell
Raw Normal View History

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE GADTs #-}
2020-12-28 20:42:38 +00:00
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
2020-12-25 16:46:02 +00:00
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.Messaging.Agent.Transmission where
import Control.Applicative ((<|>))
2020-12-25 16:46:02 +00:00
import Control.Monad.IO.Class
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
import Data.Functor
import Data.Kind
import Data.Time.Clock (UTCTime)
import Data.Time.ISO8601
import Data.Type.Equality
import Data.Typeable ()
import Network.Socket
import Numeric.Natural
import Simplex.Messaging.Agent.Store.Types
import Simplex.Messaging.Server.Transmission
( CorrId (..),
Encoded,
MsgBody,
PublicKey,
SenderId,
errBadParameters,
errMessageBody,
)
import qualified Simplex.Messaging.Server.Transmission as SMP
2020-12-25 16:46:02 +00:00
import Simplex.Messaging.Transport
2021-01-03 18:05:50 +00:00
import Simplex.Messaging.Util
2020-12-25 16:46:02 +00:00
import System.IO
2020-12-28 20:42:38 +00:00
import Text.Read
import UnliftIO.Exception
2020-12-28 20:42:38 +00:00
type ARawTransmission = (ByteString, ByteString, ByteString)
type ATransmission p = (CorrId, ConnAlias, ACommand p)
type ATransmissionOrError p = (CorrId, ConnAlias, Either ErrorType (ACommand p))
data AParty = Agent | Client
deriving (Eq, Show)
data SAParty :: AParty -> Type where
SAgent :: SAParty Agent
2020-12-28 20:42:38 +00:00
SClient :: SAParty Client
2020-12-28 20:42:38 +00:00
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
2020-12-25 16:46:02 +00:00
data ACmd where
2020-12-28 20:42:38 +00:00
ACmd :: SAParty p -> ACommand p -> ACmd
deriving instance Show ACmd
data ACommand (p :: AParty) where
NEW :: SMPServer -> ACommand Client -- response INV
2020-12-28 20:42:38 +00:00
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
READY :: ACommand Agent
-- CONF :: OtherPartyId -> ACommand Agent
-- LET :: OtherPartyId -> ACommand Client
2020-12-28 20:42:38 +00:00
SUB :: SubMode -> ACommand Client
END :: ACommand Agent
-- QST :: QueueDirection -> ACommand Client
-- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent
2020-12-28 20:42:38 +00:00
SEND :: MsgBody -> ACommand Client
MSG :: AgentMsgId -> UTCTime -> UTCTime -> MsgStatus -> MsgBody -> ACommand Agent
ACK :: AgentMsgId -> ACommand Client
-- RCVD :: AgentMsgId -> ACommand Agent
-- OFF :: ACommand Client
-- DEL :: ACommand Client
2020-12-28 20:42:38 +00:00
OK :: ACommand Agent
2020-12-25 16:46:02 +00:00
ERR :: ErrorType -> ACommand Agent
2020-12-28 20:42:38 +00:00
deriving instance Show (ACommand p)
2021-01-15 15:30:11 +00:00
type Message = ByteString
data SMPMessage
= SMPConfirmation PublicKey
| SMPMessage
{ agentMsgId :: Integer,
agentTimestamp :: UTCTime,
previousMsgHash :: ByteString,
agentMessage :: AMessage
}
deriving (Show)
data AMessage where
HELLO :: VerificationKey -> AckMode -> AMessage
REPLY :: SMPQueueInfo -> AMessage
2020-12-26 12:55:23 +00:00
A_MSG :: MsgBody -> AMessage
deriving (Show)
parseSMPMessage :: ByteString -> Either ErrorType SMPMessage
parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ SYNTAX errBadMessage
where
smpMessageP :: Parser SMPMessage
smpMessageP =
smpConfirmationP <* A.endOfLine
<|> A.endOfLine *> smpClientMessageP
smpConfirmationP :: Parser SMPMessage
smpConfirmationP = SMPConfirmation <$> ("KEY " *> base64P <* A.endOfLine)
smpClientMessageP :: Parser SMPMessage
smpClientMessageP =
SMPMessage
<$> A.decimal <* A.space
<*> tsIso8601P <* A.space
<*> base64P <* A.endOfLine
<*> agentMessageP
tsIso8601P :: Parser UTCTime
tsIso8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill (== ' ')
serializeSMPMessage :: SMPMessage -> ByteString
serializeSMPMessage = \case
SMPConfirmation sKey -> smpMessage ("KEY " <> encode sKey) "" ""
SMPMessage {agentMsgId, agentTimestamp, previousMsgHash, agentMessage} ->
let header = messageHeader agentMsgId agentTimestamp previousMsgHash
body = serializeAgentMessage agentMessage
in smpMessage "" header body
where
messageHeader msgId ts prevMsgHash =
B.unwords [B.pack $ show 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 <$> base64P <*> ackMode
reply = REPLY <$> smpQueueInfoP
a_msg = do
size :: Int <- A.decimal
A_MSG <$> (A.endOfLine *> A.take size <* A.endOfLine)
ackMode = " NO_ACK" $> AckMode Off <|> pure (AckMode On)
smpQueueInfoP :: Parser SMPQueueInfo
smpQueueInfoP =
SMPQueueInfo <$> ("smp::" *> smpServerP <* "::") <*> (base64P <* "::") <*> base64P
smpServerP :: Parser SMPServer
smpServerP = SMPServer <$> server <*> port <*> msgHash
where
server = B.unpack <$> A.takeTill (A.inClass ":# ")
port = A.char ':' *> (Just . show <$> (A.decimal :: Parser Int)) <|> pure Nothing
msgHash = A.char '#' *> (Just <$> base64P) <|> pure Nothing
base64P :: Parser ByteString
base64P = do
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
pad <- A.takeWhile (== '=')
either fail pure $ decode (str <> pad)
parseAgentMessage :: ByteString -> Either ErrorType AMessage
parseAgentMessage = parse agentMessageP $ SYNTAX errBadMessage
parse :: Parser a -> e -> (ByteString -> Either e a)
parse parser err = first (const err) . A.parseOnly (parser <* A.endOfInput)
2021-01-15 15:30:11 +00:00
errParams :: Either ErrorType a
errParams = Left $ SYNTAX errBadParameters
serializeAgentMessage :: AMessage -> ByteString
serializeAgentMessage = \case
2021-01-15 15:30:11 +00:00
HELLO _verKey _ackMode -> "HELLO" -- TODO
REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo
A_MSG msgBody -> "A_MSG " <> msgBody
2021-01-15 15:30:11 +00:00
serializeSmpQueueInfo :: SMPQueueInfo -> ByteString
serializeSmpQueueInfo (SMPQueueInfo srv qId ek) = "smp::" <> serializeServer srv <> "::" <> encode qId <> "::" <> encode ek
serializeServer :: SMPServer -> ByteString
serializeServer SMPServer {host, port, keyHash} = B.pack $ host <> maybe "" (':' :) port <> maybe "" (('#' :) . B.unpack) keyHash
data SMPServer = SMPServer
{ host :: HostName,
port :: Maybe ServiceName,
keyHash :: Maybe KeyHash
}
deriving (Eq, Ord, Show)
type KeyHash = Encoded
type ConnAlias = ByteString
type OtherPartyId = Encoded
2021-01-04 20:25:02 +00:00
data Mode = On | Off deriving (Eq, Show, Read)
2021-01-04 20:25:02 +00:00
newtype AckMode = AckMode Mode deriving (Eq, Show)
newtype SubMode = SubMode Mode deriving (Show)
data SMPQueueInfo = SMPQueueInfo SMPServer SenderId EncryptionKey
deriving (Show)
data ReplyMode = ReplyOff | ReplyOn | ReplyVia SMPServer deriving (Show)
type EncryptionKey = PublicKey
type VerificationKey = PublicKey
data QueueDirection = SND | RCV deriving (Show)
2020-12-26 12:55:23 +00:00
data QueueStatus = New | Confirmed | Secured | Active | Disabled
2021-01-04 20:25:02 +00:00
deriving (Eq, Show, Read)
type AgentMsgId = Int
data MsgStatus = MsgOk | MsgError MsgErrorType
deriving (Show)
data MsgErrorType = MsgSkipped AgentMsgId AgentMsgId | MsgBadId AgentMsgId | MsgBadHash
deriving (Show)
data ErrorType
= UNKNOWN
| UNSUPPORTED -- TODO remove once all commands implemented
| PROHIBITED
| SYNTAX Int
| BROKER Natural
| SMP SMP.ErrorType
| SIZE
| STORE StoreError
| INTERNAL -- etc. TODO SYNTAX Natural
deriving (Show, Exception)
data AckStatus = AckOk | AckError AckErrorType
deriving (Show)
data AckErrorType = AckUnknown | AckProhibited | AckSyntax Int -- etc.
deriving (Show)
2020-12-25 16:46:02 +00:00
2021-01-12 15:08:01 +00:00
errBadEncoding :: Int
errBadEncoding = 10
errBadCommand :: Int
errBadCommand = 11
errBadInvitation :: Int
2021-01-12 15:08:01 +00:00
errBadInvitation = 12
errNoConnAlias :: Int
2021-01-12 15:08:01 +00:00
errNoConnAlias = 13
errBadMessage :: Int
errBadMessage = 14
errBadServer :: Int
errBadServer = 15
smpErrTCPConnection :: Natural
smpErrTCPConnection = 1
smpErrCorrelationId :: Natural
smpErrCorrelationId = 2
smpUnexpectedResponse :: Natural
smpUnexpectedResponse = 3
parseCommandP :: Parser ACmd
parseCommandP =
"NEW " *> newCmd
<|> "INV " *> invResp
<|> "JOIN " *> joinCmd
<|> "CON" $> ACmd SAgent CON
<|> "OK" $> ACmd SAgent OK
where
newCmd = ACmd SClient . NEW <$> smpServerP
invResp = ACmd SAgent . INV <$> smpQueueInfoP
joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <*> replyMode)
replyMode =
" NO_REPLY" $> ReplyOff
<|> A.space *> (ReplyVia <$> smpServerP)
<|> pure ReplyOn
parseCommand :: ByteString -> Either ErrorType ACmd
parseCommand = parse parseCommandP $ SYNTAX errBadCommand
2020-12-25 16:46:02 +00:00
serializeCommand :: ACommand p -> ByteString
serializeCommand = \case
2021-01-15 15:30:11 +00:00
NEW srv -> "NEW " <> serializeServer srv
INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo
JOIN qInfo rMode -> "JOIN " <> serializeSmpQueueInfo qInfo <> replyMode rMode
CON -> "CON"
2021-01-12 15:08:01 +00:00
ERR e -> "ERR " <> B.pack (show e)
OK -> "OK"
c -> B.pack $ show c
where
replyMode :: ReplyMode -> ByteString
replyMode = \case
ReplyOff -> " NO_REPLY"
ReplyVia srv -> " " <> serializeServer srv
ReplyOn -> ""
2020-12-28 20:42:38 +00:00
tPutRaw :: MonadIO m => Handle -> ARawTransmission -> m ()
tPutRaw h (corrId, connAlias, command) = do
putLn h corrId
putLn h connAlias
putLn h command
tGetRaw :: MonadIO m => Handle -> m ARawTransmission
tGetRaw h = do
corrId <- getLn h
connAlias <- getLn h
command <- getLn h
return (corrId, connAlias, command)
tPut :: MonadIO m => Handle -> ATransmission p -> m ()
tPut h (corrId, connAlias, command) = tPutRaw h (bs corrId, connAlias, serializeCommand command)
-- | get client and agent transmissions
tGet :: forall m p. MonadIO m => SAParty p -> Handle -> m (ATransmissionOrError p)
tGet party h = 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 ErrorType (ACommand p)
fromParty (ACmd (p :: p1) cmd) = case testEquality party p of
Just Refl -> Right cmd
_ -> Left PROHIBITED
tConnAlias :: ARawTransmission -> ACommand p -> Either ErrorType (ACommand p)
tConnAlias (_, connAlias, _) cmd = case cmd of
2021-01-12 15:08:01 +00:00
-- NEW and JOIN have optional connAlias
NEW _ -> Right cmd
2021-01-12 15:08:01 +00:00
JOIN _ _ -> Right cmd
-- ERROR response does not always have connAlias
ERR _ -> Right cmd
-- other responses must have connAlias
_
| B.null connAlias -> Left $ SYNTAX errNoConnAlias
| otherwise -> Right cmd
cmdWithMsgBody :: ACommand p -> m (Either ErrorType (ACommand p))
cmdWithMsgBody = \case
SEND body -> SEND <$$> getMsgBody body
MSG agentMsgId srvTS agentTS status body -> MSG agentMsgId srvTS agentTS status <$$> getMsgBody body
cmd -> return $ Right cmd
getMsgBody :: MsgBody -> m (Either ErrorType MsgBody)
getMsgBody msgBody =
case B.unpack msgBody of
':' : body -> return . Right $ B.pack body
str -> case readMaybe str :: Maybe Int of
Just size -> do
body <- getBytes h size
s <- getLn h
return $ if B.null s then Right body else Left SIZE
Nothing -> return . Left $ SYNTAX errMessageBody