mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-07-02 17:52:36 +00:00
209f7826cb
* smp-server: namespaces resolver scaffolding * smp-server: Names resolver hardening + cleanup * smp-server: fuse parallel dispatchers * smp-server: JSON wire format for NameRecord + Names.hs restructure * smp-server: redact RpcAuth in Show * smp-server: JSON wire fixups + spec rewrite + small cleanups * plan: prepend implementation-diverged banner * move SimplexName into shared module * smp-server: name + contract whitelist on RSLV * smp-server: address audit findings (canonical JSON, INI guards, SSRF, TLD case, shutdown) * smp-server: round 2 audit fixes (label case, response cap, ipv6 link-local) * smp-server: round 3 audit fixes (SSRF coverage, drop noop closeManager, CSV order) * smp-server: round 4 audit fixes (0X-hex host, expanded IPv6 forms, pingEndpoint timeout) * smp-server: hardcode TldRegistries (drop registry_tld_* INI keys) * smp-server: round 6 audit fixes (IPv6 SSRF, redirects, ASCII labels) - Reject IPv6 aliases of 169.254.169.254 (IPv4-compatible / IPv4-mapped / 6to4 / NAT64) via numeric range check on parsed IPv6. - Disable HTTP redirects on the Eth RPC request. - Restrict SimplexName labels to ASCII (Cyrillic/Greek/full-width otherwise hash to different on-chain records and diverge from UTS-46 registrars). - pingEndpoint: only JsonRpcErr means "reachable"; transport/decode failures fail startup. boundedIniInt: readMaybe over partial read. - Add 127.0.0.0/8 and 0.0.0.0 to isLoopback. - Replace hand-rolled hex helpers with Data.ByteArray.Encoding; raise managerConnCount to match rpcMaxConcurrency; hex Show for NameOwner. - Fuse parallel http/https when into unless+case; drop reverse/re-reverse in mkDomain TLDWeb; first AbiInvariantViolated; Nothing <$ decodeAddress; forM_ (eitherToMaybe ...); >>= chain in NameOwner FromJSON. - Drop dead imports/exports/pragmas and two restating comments. - Tests: factor unsafeOwner/unsafeLink, addr1/2/3, testNamesConfig; add non-ASCII label rejection coverage. * namespace: bound parser input to 253 bytes (DoS defense) The bare-name fallback and bareDomain parser would otherwise consume arbitrarily many non-space bytes via takeWhile1 before any validation or length check. A crafted multi-megabyte token would be decoded as UTF-8 and re-parsed in full before being rejected. Introduce `boundedNonSpace` (scan with 253-byte cap) at the two takeWhile1 sites. Inputs longer than 253 bytes leave residue that parseOnly's implicit endOfInput rejects, so the parser fails fast without ever allocating the full input. The bound is the DNS full-domain limit, chosen for being a familiar ceiling generous enough to cover any realistic SimpleX name (longest plausible @user.subdomain.simplex stays well under 100 bytes). No per-label cap — SimpleX names don't go through DNS label resolution and there's no semantic reason to constrain individual labels. * namespace: switch to Python HTTP resolver + agent plumbing (#1796) * namespace: relax resolver_endpoint validation (path prefix, http without auth) validateUrl gains two operator-friendly relaxations and a regression test: - Allow a path prefix (e.g. https://gw.example.com:443/snrc) for a resolver behind a reverse-proxy sub-path; /resolve/<name> and /health are appended (HttpResolver already strips one trailing slash, so root and sub-path behave identically). Query/fragment/userinfo stay rejected. - Off-loopback, reject only http WITH resolver_auth (the Authorization header would travel in cleartext). http without auth is now allowed (no secret to leak; resolver data is public — also lets dev setups reach a host resolver via http://host.docker.internal). https is always allowed, with or without auth. Plain http has no response integrity; intended for trusted/local networks only. Exports validateUrl and adds validateUrlSpec (11 cases) to SMPNamesTests. * namespace: NameRecord links as arrays (multi-link, cap 5) * namespace: distinct RSLV error responses RSLV collapsed every non-hit (no resolver, malformed name, not found, backing-store failure) to ERR AUTH, so a client iterating its configured servers could not tell "this router has no resolver, try the next" from "name not registered, stop", and a transient backend error read as an authoritative miss. Names capability is runtime config, orthogonal to the linear SMP version (a future v21 router without [NAMES] must still advertise v21), so it is signalled by a command-time error like allowSMPProxy, not by the version range: no resolver configured -> ERR CMD PROHIBITED (client skips, tries next) backing-store failure -> ERR INTERNAL (transient: retry/surface) not found / malformed -> ERR AUTH (authoritative "no such name") Update the protocol spec error table and add agent tests for the no-resolver (CMD PROHIBITED) and backend-failure (INTERNAL) paths. * refactor(names): server role + one error type Addresses epoberezkin's review (PR #1784). Name resolution becomes a server role like proxy; the agent owns resolution + server selection; one error type flows through the whole stack. - ServerRoles gains `names`; UserServers gains `nameSrvs` (opt-in list); resolveSimplexName drops the explicit server arg and picks a names-capable server via getNextServer. - RSLV carries SimplexNameDomain (was RslvRequest): no JSON on the wire, contract dropped, name validated at parse (invalid -> CMD SYNTAX). - Version check moves from the encoder to Client.hs (no ERR to server). - ErrorType.NAME {nameErr :: NameErrorType} (+ AgentErrorType.NAME), wire- and JSON-encoded; resolver errors surface with diagnostics. Success response renamed NAME -> RNAME to free the collision. - NameOwner -> EthAddress (record selector); NameRecord derives FromJSON and gains field-ordered Encoding; per-field caps removed. - Remove newEnvWithNames / runSMPServerBlockingWithNames test seams; stub resolver folded into ServerConfig.namesResolverCall_. * test(server): update stats backup line count NameResolverStatsData adds 6 lines to the server stats backup (the "rslvStats:" header plus the reqs/succ/notFound/resolverErrs/disabled fields), so testRestoreMessages' expected stats-backup line count is 95 -> 101. * feat(names): public-namespace resolution via RSLV/RNAME SNRC names resolver role: RSLV command -> HTTP resolver -> RNAME record. Agent owns server selection (ServerRoles.names); NAME error family; async, concurrency-bounded resolution; length-prefixed extensible wire; spec. * remove comments Co-authored-by: Evgeny <evgeny@poberezkin.com> * simplify * move tests name * simplify: text addresses, Tail JSON, drop admitRslv * fix * remove spaghetti * reduce diff * async again, refactor * different threads limit for name resolutions * remove comment * FromField instance for SimplexNameInfo * remove comments * unStrJSON * add sameConnShortLink * remove scheme prefix * remove unused import * remove connecttarget tests * remove comment * comment --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2438 lines
81 KiB
Haskell
2438 lines
81 KiB
Haskell
{-# LANGUAGE AllowAmbiguousTypes #-}
|
|
{-# LANGUAGE BangPatterns #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
{-# LANGUAGE DerivingStrategies #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE FunctionalDependencies #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedLists #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PatternSynonyms #-}
|
|
{-# LANGUAGE PolyKinds #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE StrictData #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilyDependencies #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
{-# HLINT ignore "Use newtype instead of data" #-}
|
|
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
|
|
|
-- |
|
|
-- Module : Simplex.Messaging.ProtocolEncoding
|
|
-- Copyright : (c) simplex.chat
|
|
-- License : AGPL-3
|
|
--
|
|
-- Maintainer : chat@simplex.chat
|
|
-- Stability : experimental
|
|
-- Portability : non-portable
|
|
--
|
|
-- Types, parsers, serializers and functions to send and receive SMP protocol commands and responses.
|
|
--
|
|
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
|
module Simplex.Messaging.Protocol
|
|
( -- * SMP protocol parameters
|
|
supportedSMPClientVRange,
|
|
maxMessageLength,
|
|
paddedProxiedTLength,
|
|
e2eEncConfirmationLength,
|
|
e2eEncMessageLength,
|
|
|
|
-- * SMP protocol types
|
|
SMPClientVersion,
|
|
VersionSMPC,
|
|
VersionRangeSMPC,
|
|
pattern VersionSMPC,
|
|
ProtocolEncoding (..),
|
|
Command (..),
|
|
SubscriptionMode (..),
|
|
NewQueueReq (..),
|
|
QueueReqData (..),
|
|
QueueMode (..),
|
|
QueueLinkData,
|
|
EncFixedDataBytes,
|
|
EncUserDataBytes,
|
|
EncDataBytes (..),
|
|
NewNtfCreds (..),
|
|
ServerNtfCreds (..),
|
|
Party (..),
|
|
Cmd (..),
|
|
QueueParty,
|
|
BatchParty,
|
|
ServiceParty,
|
|
ASubscriberParty (..),
|
|
BrokerMsg (..),
|
|
SParty (..),
|
|
PartyI (..),
|
|
QueueIdsKeys (..),
|
|
ProtocolErrorType (..),
|
|
ErrorType (..),
|
|
CommandError (..),
|
|
ProxyError (..),
|
|
NameErrorType (..),
|
|
BrokerErrorType (..),
|
|
NetworkError (..),
|
|
BlockingInfo (..),
|
|
BlockingReason (..),
|
|
RawTransmission,
|
|
Transmission,
|
|
TAuthorizations,
|
|
TransmissionAuth (..),
|
|
SignedTransmission,
|
|
SignedTransmissionOrError,
|
|
SentRawTransmission,
|
|
ClientMsgEnvelope (..),
|
|
PubHeader (..),
|
|
ClientMessage (..),
|
|
PrivHeader (..),
|
|
Protocol (..),
|
|
ProtocolType (..),
|
|
SProtocolType (..),
|
|
AProtocolType (..),
|
|
ProtocolTypeI (..),
|
|
UserProtocol,
|
|
ProtocolServer (..),
|
|
ProtoServer,
|
|
SMPServer,
|
|
pattern SMPServer,
|
|
SMPServerWithAuth,
|
|
NtfServer,
|
|
pattern NtfServer,
|
|
NtfServerWithAuth,
|
|
XFTPServer,
|
|
pattern XFTPServer,
|
|
XFTPServerWithAuth,
|
|
ProtoServerWithAuth (..),
|
|
AProtoServerWithAuth (..),
|
|
BasicAuth (..),
|
|
SrvLoc (..),
|
|
CorrId (..),
|
|
pattern NoCorrId,
|
|
EntityId (..),
|
|
pattern NoEntity,
|
|
QueueId,
|
|
RecipientId,
|
|
SenderId,
|
|
LinkId,
|
|
NotifierId,
|
|
ServiceId,
|
|
RcvPrivateAuthKey,
|
|
RcvPublicAuthKey,
|
|
RcvPublicDhKey,
|
|
RcvDhSecret,
|
|
SndPrivateAuthKey,
|
|
SndPublicAuthKey,
|
|
NtfPrivateAuthKey,
|
|
NtfPublicAuthKey,
|
|
RcvNtfPublicDhKey,
|
|
RcvNtfDhSecret,
|
|
Message (..),
|
|
RcvMessage (..),
|
|
MsgId,
|
|
MsgBody,
|
|
IdsHash (..),
|
|
ServiceSub (..),
|
|
ServiceSubResult (..),
|
|
ServiceSubError (..),
|
|
serviceSubResult,
|
|
queueIdsHash,
|
|
queueIdHash,
|
|
addServiceSubs,
|
|
subtractServiceSubs,
|
|
MaxMessageLen,
|
|
MaxRcvMessageLen,
|
|
EncRcvMsgBody (..),
|
|
RcvMsgBody (..),
|
|
ClientRcvMsgBody (..),
|
|
EncNMsgMeta,
|
|
SMPMsgMeta (..),
|
|
NMsgMeta (..),
|
|
EncFwdResponse (..),
|
|
EncFwdTransmission (..),
|
|
EncResponse (..),
|
|
EncTransmission (..),
|
|
FwdResponse (..),
|
|
FwdTransmission (..),
|
|
NameRecord (..),
|
|
MsgFlags (..),
|
|
initialSMPClientVersion,
|
|
currentSMPClientVersion,
|
|
senderCanSecure,
|
|
queueReqMode,
|
|
queueParty,
|
|
batchParty,
|
|
serviceParty,
|
|
partyClientRole,
|
|
partyServiceRole,
|
|
userProtocol,
|
|
rcvMessageMeta,
|
|
noMsgFlags,
|
|
messageId,
|
|
messageTs,
|
|
toNetworkError,
|
|
|
|
-- * Parse and serialize
|
|
ProtocolMsgTag (..),
|
|
messageTagP,
|
|
TransmissionForAuth (..),
|
|
encodeTransmissionForAuth,
|
|
encodeTransmission,
|
|
transmissionP,
|
|
_smpP,
|
|
encodeRcvMsgBody,
|
|
clientRcvMsgBodyP,
|
|
legacyEncodeServer,
|
|
legacyServerP,
|
|
legacyStrEncodeServer,
|
|
srvHostnamesSMPClientVersion,
|
|
sndAuthKeySMPClientVersion,
|
|
shortLinksSMPClientVersion,
|
|
sameSrvAddr,
|
|
sameSrvAddr',
|
|
noAuthSrv,
|
|
toMsgInfo,
|
|
|
|
-- * TCP transport functions
|
|
TransportBatch (..),
|
|
tPut,
|
|
tPutLog,
|
|
tGetServer,
|
|
tGetClient,
|
|
tParse,
|
|
tDecodeServer,
|
|
tDecodeClient,
|
|
tEncode,
|
|
tEncodeBatch1,
|
|
batchTransmissions,
|
|
batchTransmissions',
|
|
batchTransmissions_,
|
|
|
|
-- * exports for tests
|
|
CommandTag (..),
|
|
BrokerMsgTag (..),
|
|
checkParty,
|
|
)
|
|
where
|
|
|
|
import Control.Applicative (optional, (<|>))
|
|
import Control.Exception (Exception, SomeException, displayException, fromException)
|
|
import Control.Monad.Except
|
|
import Data.Aeson (FromJSON (..), ToJSON (..))
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.TH as J
|
|
import Data.Attoparsec.ByteString.Char8 (Parser, (<?>))
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.Bifunctor (bimap, first)
|
|
import Data.Bits (xor)
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.ByteString.Base64 as B64
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import qualified Data.ByteString.Lazy as LB
|
|
import Data.Char (isPrint, isSpace)
|
|
import Data.Constraint (Dict (..))
|
|
import Data.Functor (($>))
|
|
import Data.Int (Int64)
|
|
import Data.Kind
|
|
import Data.List (foldl')
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import qualified Data.List.NonEmpty as L
|
|
import Data.Maybe (isJust, isNothing)
|
|
import Data.String
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
|
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
|
|
import Data.Type.Equality
|
|
import Data.Word (Word8, Word16)
|
|
import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
|
|
import qualified GHC.TypeLits as TE
|
|
import qualified GHC.TypeLits as Type
|
|
import Network.Socket (ServiceName)
|
|
import qualified Network.TLS as TLS
|
|
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..))
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Names.Record (NameRecord (..))
|
|
import Simplex.Messaging.Parsers
|
|
import Simplex.Messaging.Protocol.Types
|
|
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
|
import Simplex.Messaging.ServiceScheme
|
|
import Simplex.Messaging.SimplexName (SimplexNameDomain)
|
|
import Simplex.Messaging.Transport
|
|
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..))
|
|
import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>))
|
|
import Simplex.Messaging.Version
|
|
import Simplex.Messaging.Version.Internal
|
|
|
|
-- SMP client protocol version history:
|
|
-- 1 - binary protocol encoding (1/1/2022)
|
|
-- 2 - multiple server hostnames and versioned queue addresses (8/12/2022)
|
|
-- 3 - faster handshake: SKEY command for sender to secure queue (6/30/2024, SMP protocol version 9)
|
|
-- 4 - short connection links with stored data (3/30/2025, SMP protocol version 15)
|
|
|
|
data SMPClientVersion
|
|
|
|
instance VersionScope SMPClientVersion
|
|
|
|
type VersionSMPC = Version SMPClientVersion
|
|
|
|
type VersionRangeSMPC = VersionRange SMPClientVersion
|
|
|
|
pattern VersionSMPC :: Word16 -> VersionSMPC
|
|
pattern VersionSMPC v = Version v
|
|
|
|
initialSMPClientVersion :: VersionSMPC
|
|
initialSMPClientVersion = VersionSMPC 1
|
|
|
|
srvHostnamesSMPClientVersion :: VersionSMPC
|
|
srvHostnamesSMPClientVersion = VersionSMPC 2
|
|
|
|
sndAuthKeySMPClientVersion :: VersionSMPC
|
|
sndAuthKeySMPClientVersion = VersionSMPC 3
|
|
|
|
shortLinksSMPClientVersion :: VersionSMPC
|
|
shortLinksSMPClientVersion = VersionSMPC 4
|
|
|
|
currentSMPClientVersion :: VersionSMPC
|
|
currentSMPClientVersion = VersionSMPC 4
|
|
|
|
supportedSMPClientVRange :: VersionRangeSMPC
|
|
supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion
|
|
|
|
-- TODO v6.0 remove dependency on version
|
|
maxMessageLength :: VersionSMP -> Int
|
|
maxMessageLength v
|
|
| v >= encryptedBlockSMPVersion = 16048 -- max 16048
|
|
| v >= sendingProxySMPVersion = 16064 -- max 16067
|
|
| otherwise = 16088 -- 16048 - always use this size to determine allowed ranges
|
|
|
|
paddedProxiedTLength :: Int
|
|
paddedProxiedTLength = 16226 -- 16225 .. 16227
|
|
|
|
-- TODO v7.0 change to 16048
|
|
type MaxMessageLen = 16088
|
|
|
|
-- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used)
|
|
type MaxRcvMessageLen = MaxMessageLen + 16 -- 16104, the padded size is 16106
|
|
|
|
-- it is shorter to allow per-queue e2e encryption DH key in the "public" header
|
|
e2eEncConfirmationLength :: Int
|
|
e2eEncConfirmationLength = 15904 -- 15865 .. 15960
|
|
|
|
e2eEncMessageLength :: Int
|
|
e2eEncMessageLength = 16000 -- 15988 .. 16005
|
|
|
|
-- | SMP protocol clients
|
|
data Party
|
|
= Creator
|
|
| Recipient
|
|
| RecipientService
|
|
| Sender
|
|
| IdleClient
|
|
| Notifier
|
|
| NotifierService
|
|
| LinkClient
|
|
| ProxiedClient
|
|
| ProxyService
|
|
| Resolver
|
|
deriving (Show)
|
|
|
|
-- | Singleton types for SMP protocol clients
|
|
data SParty :: Party -> Type where
|
|
SCreator :: SParty Creator
|
|
SRecipient :: SParty Recipient
|
|
SRecipientService :: SParty RecipientService
|
|
SSender :: SParty Sender
|
|
SIdleClient :: SParty IdleClient
|
|
SNotifier :: SParty Notifier
|
|
SNotifierService :: SParty NotifierService
|
|
SSenderLink :: SParty LinkClient
|
|
SProxiedClient :: SParty ProxiedClient
|
|
SProxyService :: SParty ProxyService
|
|
SResolver :: SParty Resolver
|
|
|
|
instance TestEquality SParty where
|
|
testEquality SCreator SCreator = Just Refl
|
|
testEquality SRecipient SRecipient = Just Refl
|
|
testEquality SRecipientService SRecipientService = Just Refl
|
|
testEquality SSender SSender = Just Refl
|
|
testEquality SIdleClient SIdleClient = Just Refl
|
|
testEquality SNotifier SNotifier = Just Refl
|
|
testEquality SNotifierService SNotifierService = Just Refl
|
|
testEquality SSenderLink SSenderLink = Just Refl
|
|
testEquality SProxiedClient SProxiedClient = Just Refl
|
|
testEquality SProxyService SProxyService = Just Refl
|
|
testEquality SResolver SResolver = Just Refl
|
|
testEquality _ _ = Nothing
|
|
|
|
deriving instance Show (SParty p)
|
|
|
|
class PartyI (p :: Party) where sParty :: SParty p
|
|
|
|
instance PartyI Creator where sParty = SCreator
|
|
|
|
instance PartyI Recipient where sParty = SRecipient
|
|
|
|
instance PartyI RecipientService where sParty = SRecipientService
|
|
|
|
instance PartyI Sender where sParty = SSender
|
|
|
|
instance PartyI IdleClient where sParty = SIdleClient
|
|
|
|
instance PartyI Notifier where sParty = SNotifier
|
|
|
|
instance PartyI NotifierService where sParty = SNotifierService
|
|
|
|
instance PartyI LinkClient where sParty = SSenderLink
|
|
|
|
instance PartyI ProxiedClient where sParty = SProxiedClient
|
|
|
|
instance PartyI ProxyService where sParty = SProxyService
|
|
|
|
instance PartyI Resolver where sParty = SResolver
|
|
|
|
-- command parties that can read queues
|
|
type family QueueParty (p :: Party) :: Constraint where
|
|
QueueParty Recipient = ()
|
|
QueueParty Sender = ()
|
|
QueueParty Notifier = ()
|
|
QueueParty LinkClient = ()
|
|
QueueParty p =
|
|
(Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not QueueParty"))
|
|
|
|
queueParty :: SParty p -> Maybe (Dict (PartyI p, QueueParty p))
|
|
queueParty = \case
|
|
SRecipient -> Just Dict
|
|
SSender -> Just Dict
|
|
SSenderLink -> Just Dict
|
|
SNotifier -> Just Dict
|
|
_ -> Nothing
|
|
{-# INLINE queueParty #-}
|
|
|
|
type family BatchParty (p :: Party) :: Constraint where
|
|
BatchParty Recipient = ()
|
|
BatchParty Notifier = ()
|
|
BatchParty p =
|
|
(Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not BatchParty"))
|
|
|
|
batchParty :: SParty p -> Maybe (Dict (PartyI p, BatchParty p))
|
|
batchParty = \case
|
|
SRecipient -> Just Dict
|
|
SNotifier -> Just Dict
|
|
_ -> Nothing
|
|
{-# INLINE batchParty #-}
|
|
|
|
-- command parties that can subscribe to individual queues
|
|
type family ServiceParty (p :: Party) :: Constraint where
|
|
ServiceParty RecipientService = ()
|
|
ServiceParty NotifierService = ()
|
|
ServiceParty p =
|
|
(Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not ServiceParty"))
|
|
|
|
serviceParty :: SParty p -> Maybe (Dict (PartyI p, ServiceParty p))
|
|
serviceParty = \case
|
|
SRecipientService -> Just Dict
|
|
SNotifierService -> Just Dict
|
|
_ -> Nothing
|
|
{-# INLINE serviceParty #-}
|
|
|
|
data ASubscriberParty = forall p. (PartyI p, ServiceParty p) => ASP (SParty p)
|
|
|
|
deriving instance Show ASubscriberParty
|
|
|
|
instance Eq ASubscriberParty where
|
|
ASP p == ASP p' = isJust $ testEquality p p'
|
|
|
|
instance Encoding ASubscriberParty where
|
|
smpEncode = \case
|
|
ASP SRecipientService -> "R"
|
|
ASP SNotifierService -> "N"
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'R' -> pure $ ASP SRecipientService
|
|
'N' -> pure $ ASP SNotifierService
|
|
_ -> fail "bad ASubscriberParty"
|
|
|
|
instance StrEncoding ASubscriberParty where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
partyClientRole :: SParty p -> Maybe SMPServiceRole
|
|
partyClientRole = \case
|
|
SCreator -> Just SRMessaging
|
|
SRecipient -> Just SRMessaging
|
|
SRecipientService -> Just SRMessaging
|
|
SSender -> Just SRMessaging
|
|
SIdleClient -> Nothing
|
|
SNotifier -> Just SRNotifier
|
|
SNotifierService -> Just SRNotifier
|
|
SSenderLink -> Just SRMessaging
|
|
SProxiedClient -> Just SRMessaging
|
|
SProxyService -> Just SRProxy
|
|
SResolver -> Just SRMessaging
|
|
{-# INLINE partyClientRole #-}
|
|
|
|
partyServiceRole :: ServiceParty p => SParty p -> SMPServiceRole
|
|
partyServiceRole = \case
|
|
SRecipientService -> SRMessaging
|
|
SNotifierService -> SRNotifier
|
|
{-# INLINE partyServiceRole #-}
|
|
|
|
-- | Type for client command of any participant.
|
|
data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p)
|
|
|
|
deriving instance Show Cmd
|
|
|
|
-- | Parsed SMP transmission without signature, size and session ID.
|
|
type Transmission c = (CorrId, EntityId, c)
|
|
|
|
-- | signed parsed transmission, with original raw bytes and parsing error.
|
|
type SignedTransmission c = (Maybe TAuthorizations, Signed, Transmission c)
|
|
|
|
type SignedTransmissionOrError e c = Either (Transmission e) (SignedTransmission c)
|
|
|
|
type Signed = ByteString
|
|
|
|
-- | unparsed SMP transmission with signature.
|
|
data RawTransmission = RawTransmission
|
|
{ authenticator :: ByteString, -- signature or encrypted transmission hash
|
|
serviceSig :: Maybe (C.Signature 'C.Ed25519), -- optional second signature with the key of the client service
|
|
authorized :: ByteString, -- authorized transmission
|
|
sessId :: SessionId,
|
|
corrId :: CorrId,
|
|
entityId :: EntityId,
|
|
command :: ByteString
|
|
}
|
|
deriving (Show)
|
|
|
|
type TAuthorizations = (TransmissionAuth, Maybe (C.Signature 'C.Ed25519))
|
|
|
|
data TransmissionAuth
|
|
= TASignature C.ASignature
|
|
| TAAuthenticator C.CbAuthenticator
|
|
deriving (Show)
|
|
|
|
-- this encoding is backwards compatible with v6 that used Maybe C.ASignature instead of TransmissionAuth
|
|
tEncodeAuth :: Bool -> Maybe TAuthorizations -> ByteString
|
|
tEncodeAuth serviceAuth = \case
|
|
Nothing -> smpEncode B.empty
|
|
Just (auth, sig)
|
|
| serviceAuth -> smpEncode (authBytes auth, sig)
|
|
| otherwise -> smpEncode (authBytes auth)
|
|
where
|
|
authBytes = \case
|
|
TASignature s -> C.signatureBytes s
|
|
TAAuthenticator (C.CbAuthenticator s) -> s
|
|
|
|
decodeTAuthBytes :: ByteString -> Maybe (C.Signature 'C.Ed25519) -> Either String (Maybe TAuthorizations)
|
|
decodeTAuthBytes s serviceSig
|
|
| B.null s = Right Nothing
|
|
| B.length s == C.cbAuthenticatorSize = Right $ Just (TAAuthenticator (C.CbAuthenticator s), serviceSig)
|
|
| otherwise = (\sig -> Just (TASignature sig, serviceSig)) <$> C.decodeSignature s
|
|
|
|
-- | unparsed sent SMP transmission with signature.
|
|
type SentRawTransmission = (Maybe TAuthorizations, ByteString)
|
|
|
|
-- | SMP queue ID for the recipient.
|
|
type RecipientId = QueueId
|
|
|
|
-- | SMP queue ID for the sender.
|
|
type SenderId = QueueId
|
|
|
|
-- | SMP queue ID for notifications.
|
|
type NotifierId = QueueId
|
|
|
|
type LinkId = QueueId
|
|
|
|
-- | SMP queue ID on the server.
|
|
type QueueId = EntityId
|
|
|
|
-- | Parameterized type for SMP protocol commands from all clients.
|
|
data Command (p :: Party) where
|
|
-- SMP recipient commands
|
|
-- RcvPublicAuthKey is the key used for command authorization:
|
|
-- v6 of SMP servers only support signature algorithm for command authorization.
|
|
-- v7 of SMP servers additionally support additional layer of authenticated encryption.
|
|
-- RcvPublicAuthKey is defined as C.APublicKey - it can be either signature or DH public keys.
|
|
NEW :: NewQueueReq -> Command Creator
|
|
SUB :: Command Recipient
|
|
-- | subscribe all associated queues. Service ID must be used as entity ID, and service session key must sign the command.
|
|
-- Parameters are expected queue count and hash of all subscribed queues, it allows to monitor "state drift" on the server
|
|
SUBS :: Int64 -> IdsHash -> Command RecipientService
|
|
KEY :: SndPublicAuthKey -> Command Recipient
|
|
RKEY :: NonEmpty RcvPublicAuthKey -> Command Recipient
|
|
LSET :: LinkId -> QueueLinkData -> Command Recipient
|
|
LDEL :: Command Recipient
|
|
NKEY :: NtfPublicAuthKey -> RcvNtfPublicDhKey -> Command Recipient
|
|
NDEL :: Command Recipient
|
|
GET :: Command Recipient
|
|
ACK :: MsgId -> Command Recipient
|
|
OFF :: Command Recipient
|
|
DEL :: Command Recipient
|
|
QUE :: Command Recipient
|
|
-- SMP sender commands
|
|
SKEY :: SndPublicAuthKey -> Command Sender
|
|
-- SEND v1 has to be supported for encoding/decoding
|
|
-- SEND :: MsgBody -> Command Sender
|
|
SEND :: MsgFlags -> MsgBody -> Command Sender
|
|
PING :: Command IdleClient
|
|
-- Client accessing short links
|
|
LKEY :: SndPublicAuthKey -> Command LinkClient
|
|
LGET :: Command LinkClient
|
|
-- SMP notification subscriber commands
|
|
NSUB :: Command Notifier
|
|
-- | subscribe all associated queues. Service ID must be used as entity ID, and service session key must sign the command.
|
|
NSUBS :: Int64 -> IdsHash -> Command NotifierService
|
|
PRXY :: SMPServer -> Maybe BasicAuth -> Command ProxiedClient -- request a relay server connection by URI
|
|
-- Transmission to proxy:
|
|
-- - entity ID: ID of the session with relay returned in PKEY (response to PRXY)
|
|
-- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay
|
|
-- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response
|
|
-- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection.
|
|
PFWD :: VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy
|
|
-- Transmission forwarded to relay:
|
|
-- - entity ID: empty
|
|
-- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission
|
|
RFWD :: EncFwdTransmission -> Command ProxyService -- use CorrId as CbNonce, proxy to relay
|
|
-- Resolve SimpleX name.
|
|
RSLV :: SimplexNameDomain -> Command Resolver
|
|
|
|
deriving instance Show (Command p)
|
|
|
|
data NewQueueReq = NewQueueReq
|
|
{ rcvAuthKey :: RcvPublicAuthKey,
|
|
rcvDhKey :: RcvPublicDhKey,
|
|
auth_ :: Maybe BasicAuth,
|
|
subMode :: SubscriptionMode,
|
|
queueReqData :: Maybe QueueReqData,
|
|
ntfCreds :: Maybe NewNtfCreds
|
|
}
|
|
deriving (Show)
|
|
|
|
data SubscriptionMode = SMSubscribe | SMOnlyCreate
|
|
deriving (Eq, Show)
|
|
|
|
-- SenderId must be computed client-side as the first 24 bytes of `sha3-384(corr_id)`, `corr_id` - a random transmission ID.
|
|
-- The server must verify and reject it if it does not match (and in case of collision).
|
|
-- This allows to include SenderId in FixedDataBytes in full connection request,
|
|
-- and at the same time prevents the possibility of checking whether a queue with a known ID exists.
|
|
data QueueReqData = QRMessaging (Maybe (SenderId, QueueLinkData)) | QRContact (Maybe (LinkId, (SenderId, QueueLinkData)))
|
|
deriving (Show)
|
|
|
|
queueReqMode :: QueueReqData -> QueueMode
|
|
queueReqMode = \case
|
|
QRMessaging _ -> QMMessaging
|
|
QRContact _ -> QMContact
|
|
|
|
senderCanSecure :: Maybe QueueMode -> Bool
|
|
senderCanSecure = \case
|
|
Just QMMessaging -> True
|
|
_ -> False
|
|
|
|
type QueueLinkData = (EncFixedDataBytes, EncUserDataBytes)
|
|
|
|
type EncFixedDataBytes = EncDataBytes
|
|
|
|
type EncUserDataBytes = EncDataBytes
|
|
|
|
newtype EncDataBytes = EncDataBytes ByteString
|
|
deriving (Eq, Show)
|
|
deriving newtype (FromField, StrEncoding)
|
|
|
|
instance Encoding EncDataBytes where
|
|
smpEncode (EncDataBytes s) = smpEncode (Large s)
|
|
{-# INLINE smpEncode #-}
|
|
smpP = EncDataBytes . unLarge <$> smpP
|
|
{-# INLINE smpP #-}
|
|
|
|
instance ToField EncDataBytes where
|
|
toField (EncDataBytes s) = toField (Binary s)
|
|
{-# INLINE toField #-}
|
|
|
|
data NewNtfCreds = NewNtfCreds NtfPublicAuthKey RcvNtfPublicDhKey deriving (Show)
|
|
|
|
instance StrEncoding SubscriptionMode where
|
|
strEncode = \case
|
|
SMSubscribe -> "subscribe"
|
|
SMOnlyCreate -> "only-create"
|
|
strP =
|
|
(A.string "subscribe" $> SMSubscribe)
|
|
<|> (A.string "only-create" $> SMOnlyCreate)
|
|
<?> "SubscriptionMode"
|
|
|
|
instance Encoding SubscriptionMode where
|
|
smpEncode = \case
|
|
SMSubscribe -> "S"
|
|
SMOnlyCreate -> "C"
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'S' -> pure SMSubscribe
|
|
'C' -> pure SMOnlyCreate
|
|
_ -> fail "bad SubscriptionMode"
|
|
|
|
instance Encoding QueueReqData where
|
|
smpEncode = \case
|
|
QRMessaging d -> smpEncode ('M', d)
|
|
QRContact d -> smpEncode ('C', d)
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'M' -> QRMessaging <$> smpP
|
|
'C' -> QRContact <$> smpP
|
|
_ -> fail "bad QueueReqData"
|
|
|
|
instance Encoding NewNtfCreds where
|
|
smpEncode (NewNtfCreds authKey dhKey) = smpEncode (authKey, dhKey)
|
|
smpP = NewNtfCreds <$> smpP <*> smpP
|
|
|
|
newtype EncTransmission = EncTransmission ByteString
|
|
deriving (Show)
|
|
|
|
data FwdTransmission = FwdTransmission
|
|
{ fwdCorrId :: CorrId,
|
|
fwdVersion :: VersionSMP,
|
|
fwdKey :: C.PublicKeyX25519,
|
|
fwdTransmission :: EncTransmission
|
|
}
|
|
|
|
instance Encoding FwdTransmission where
|
|
smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
|
|
smpEncode (corrId, fwdVersion, fwdKey, Tail t)
|
|
smpP = do
|
|
(corrId, fwdVersion, fwdKey, Tail t) <- smpP
|
|
pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
|
|
|
|
newtype EncFwdTransmission = EncFwdTransmission ByteString
|
|
deriving (Show)
|
|
|
|
data BrokerMsg where
|
|
-- SMP broker messages (responses, client messages, notifications)
|
|
IDS :: QueueIdsKeys -> BrokerMsg
|
|
LNK :: SenderId -> QueueLinkData -> BrokerMsg
|
|
-- | Service subscription success - confirms when queue was associated with the service
|
|
SOK :: Maybe ServiceId -> BrokerMsg
|
|
-- | The number of queues and XOR-hash of their IDs subscribed with SUBS command
|
|
SOKS :: Int64 -> IdsHash -> BrokerMsg
|
|
-- MSG v1/2 has to be supported for encoding/decoding
|
|
-- v1: MSG :: MsgId -> SystemTime -> MsgBody -> BrokerMsg
|
|
-- v2: MsgId -> SystemTime -> MsgFlags -> MsgBody -> BrokerMsg
|
|
MSG :: RcvMessage -> BrokerMsg
|
|
-- sent once delivering messages to SUBS command is complete
|
|
ALLS :: BrokerMsg
|
|
NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg
|
|
NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg
|
|
-- Should include certificate chain
|
|
PKEY :: SessionId -> VersionRangeSMP -> CertChainPubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key
|
|
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
|
|
PRES :: EncResponse -> BrokerMsg -- proxy to client
|
|
END :: BrokerMsg
|
|
ENDS :: Int64 -> IdsHash -> BrokerMsg
|
|
DELD :: BrokerMsg
|
|
INFO :: QueueInfo -> BrokerMsg
|
|
OK :: BrokerMsg
|
|
ERR :: ErrorType -> BrokerMsg
|
|
PONG :: BrokerMsg
|
|
-- Resolved SimpleX name.
|
|
RNAME :: NameRecord -> BrokerMsg
|
|
deriving (Eq, Show)
|
|
|
|
data RcvMessage = RcvMessage
|
|
{ msgId :: MsgId,
|
|
msgBody :: EncRcvMsgBody -- e2e encrypted, with extra encryption for recipient
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
newtype EncFwdResponse = EncFwdResponse ByteString
|
|
deriving (Eq, Show)
|
|
|
|
data FwdResponse = FwdResponse
|
|
{ fwdCorrId :: CorrId,
|
|
fwdResponse :: EncResponse
|
|
}
|
|
|
|
instance Encoding FwdResponse where
|
|
smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} =
|
|
smpEncode (corrId, Tail t)
|
|
smpP = do
|
|
(corrId, Tail t) <- smpP
|
|
pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t}
|
|
|
|
newtype EncResponse = EncResponse ByteString
|
|
deriving (Eq, Show)
|
|
|
|
-- | received message without server/recipient encryption
|
|
data Message
|
|
= Message
|
|
{ msgId :: MsgId,
|
|
msgTs :: SystemTime,
|
|
msgFlags :: MsgFlags,
|
|
msgBody :: C.MaxLenBS MaxMessageLen
|
|
}
|
|
| MessageQuota
|
|
{ msgId :: MsgId,
|
|
msgTs :: SystemTime
|
|
}
|
|
|
|
toMsgInfo :: Message -> MsgInfo
|
|
toMsgInfo = \case
|
|
Message {msgId, msgTs} -> msgInfo msgId msgTs MTMessage
|
|
MessageQuota {msgId, msgTs} -> msgInfo msgId msgTs MTQuota
|
|
where
|
|
msgInfo msgId msgTs msgType = MsgInfo {msgId = decodeLatin1 $ B64.encode msgId, msgTs = systemToUTCTime msgTs, msgType}
|
|
|
|
messageId :: Message -> MsgId
|
|
messageId = \case
|
|
Message {msgId} -> msgId
|
|
MessageQuota {msgId} -> msgId
|
|
{-# INLINE messageId #-}
|
|
|
|
messageTs :: Message -> SystemTime
|
|
messageTs = \case
|
|
Message {msgTs} -> msgTs
|
|
MessageQuota {msgTs} -> msgTs
|
|
{-# INLINE messageTs #-}
|
|
|
|
newtype EncRcvMsgBody = EncRcvMsgBody ByteString
|
|
deriving (Eq, Show)
|
|
|
|
data RcvMsgBody
|
|
= RcvMsgBody
|
|
{ msgTs :: SystemTime,
|
|
msgFlags :: MsgFlags,
|
|
msgBody :: C.MaxLenBS MaxMessageLen
|
|
}
|
|
| RcvMsgQuota
|
|
{ msgTs :: SystemTime
|
|
}
|
|
|
|
msgQuotaTag :: ByteString
|
|
msgQuotaTag = "QUOTA"
|
|
|
|
encodeRcvMsgBody :: RcvMsgBody -> C.MaxLenBS MaxRcvMessageLen
|
|
encodeRcvMsgBody = \case
|
|
RcvMsgBody {msgTs, msgFlags, msgBody} ->
|
|
let rcvMeta :: C.MaxLenBS 16 = C.unsafeMaxLenBS $ smpEncode (msgTs, msgFlags, ' ')
|
|
in C.appendMaxLenBS rcvMeta msgBody
|
|
RcvMsgQuota {msgTs} ->
|
|
C.unsafeMaxLenBS $ msgQuotaTag <> " " <> smpEncode msgTs
|
|
|
|
data ClientRcvMsgBody
|
|
= ClientRcvMsgBody
|
|
{ msgTs :: SystemTime,
|
|
msgFlags :: MsgFlags,
|
|
msgBody :: ByteString
|
|
}
|
|
| ClientRcvMsgQuota
|
|
{ msgTs :: SystemTime
|
|
}
|
|
|
|
clientRcvMsgBodyP :: Parser ClientRcvMsgBody
|
|
clientRcvMsgBodyP = msgQuotaP <|> msgBodyP
|
|
where
|
|
msgQuotaP = A.string msgQuotaTag *> (ClientRcvMsgQuota <$> _smpP)
|
|
msgBodyP = do
|
|
msgTs <- smpP
|
|
msgFlags <- smpP
|
|
Tail msgBody <- _smpP
|
|
pure ClientRcvMsgBody {msgTs, msgFlags, msgBody}
|
|
|
|
instance StrEncoding Message where
|
|
strEncode = \case
|
|
Message {msgId, msgTs, msgFlags, msgBody} ->
|
|
B.unwords
|
|
[ strEncode msgId,
|
|
strEncode msgTs,
|
|
"flags=" <> strEncode msgFlags,
|
|
strEncode msgBody
|
|
]
|
|
MessageQuota {msgId, msgTs} ->
|
|
B.unwords
|
|
[ strEncode msgId,
|
|
strEncode msgTs,
|
|
"quota"
|
|
]
|
|
strP = do
|
|
msgId <- strP_
|
|
msgTs <- strP_
|
|
msgQuotaP msgId msgTs <|> msgP msgId msgTs
|
|
where
|
|
msgQuotaP msgId msgTs = "quota" $> MessageQuota {msgId, msgTs}
|
|
msgP msgId msgTs = do
|
|
msgFlags <- ("flags=" *> strP_) <|> pure noMsgFlags
|
|
msgBody <- strP
|
|
pure Message {msgId, msgTs, msgFlags, msgBody}
|
|
|
|
type EncNMsgMeta = ByteString
|
|
|
|
data SMPMsgMeta = SMPMsgMeta
|
|
{ msgId :: MsgId,
|
|
msgTs :: SystemTime,
|
|
msgFlags :: MsgFlags
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
instance StrEncoding SMPMsgMeta where
|
|
strEncode SMPMsgMeta {msgId, msgTs, msgFlags} =
|
|
strEncode (msgId, msgTs, msgFlags)
|
|
strP = do
|
|
(msgId, msgTs, msgFlags) <- strP
|
|
pure SMPMsgMeta {msgId, msgTs, msgFlags}
|
|
|
|
rcvMessageMeta :: MsgId -> ClientRcvMsgBody -> SMPMsgMeta
|
|
rcvMessageMeta msgId = \case
|
|
ClientRcvMsgBody {msgTs, msgFlags} -> SMPMsgMeta {msgId, msgTs, msgFlags}
|
|
ClientRcvMsgQuota {msgTs} -> SMPMsgMeta {msgId, msgTs, msgFlags = noMsgFlags}
|
|
|
|
data NMsgMeta = NMsgMeta
|
|
{ msgId :: MsgId,
|
|
msgTs :: SystemTime
|
|
}
|
|
deriving (Show)
|
|
|
|
instance Encoding NMsgMeta where
|
|
smpEncode NMsgMeta {msgId, msgTs} =
|
|
smpEncode (msgId, msgTs)
|
|
smpP = do
|
|
-- Tail here is to allow extension in the future clients/servers
|
|
(msgId, msgTs, Tail _) <- smpP
|
|
pure NMsgMeta {msgId, msgTs}
|
|
|
|
-- it must be data for correct JSON encoding
|
|
data MsgFlags = MsgFlags {notification :: Bool}
|
|
deriving (Eq, Show)
|
|
|
|
-- this encoding should not become bigger than 7 bytes (currently it is 1 byte)
|
|
instance Encoding MsgFlags where
|
|
smpEncode MsgFlags {notification} = smpEncode notification
|
|
smpP = do
|
|
notification <- smpP <* A.takeTill (== ' ')
|
|
pure MsgFlags {notification}
|
|
|
|
instance StrEncoding MsgFlags where
|
|
strEncode = smpEncode
|
|
{-# INLINE strEncode #-}
|
|
strP = smpP
|
|
{-# INLINE strP #-}
|
|
|
|
noMsgFlags :: MsgFlags
|
|
noMsgFlags = MsgFlags {notification = False}
|
|
|
|
-- * SMP command tags
|
|
|
|
data CommandTag (p :: Party) where
|
|
NEW_ :: CommandTag Creator
|
|
SUB_ :: CommandTag Recipient
|
|
SUBS_ :: CommandTag RecipientService
|
|
KEY_ :: CommandTag Recipient
|
|
RKEY_ :: CommandTag Recipient
|
|
LSET_ :: CommandTag Recipient
|
|
LDEL_ :: CommandTag Recipient
|
|
NKEY_ :: CommandTag Recipient
|
|
NDEL_ :: CommandTag Recipient
|
|
GET_ :: CommandTag Recipient
|
|
ACK_ :: CommandTag Recipient
|
|
OFF_ :: CommandTag Recipient
|
|
DEL_ :: CommandTag Recipient
|
|
QUE_ :: CommandTag Recipient
|
|
SKEY_ :: CommandTag Sender
|
|
SEND_ :: CommandTag Sender
|
|
PING_ :: CommandTag IdleClient
|
|
LKEY_ :: CommandTag LinkClient
|
|
LGET_ :: CommandTag LinkClient
|
|
PRXY_ :: CommandTag ProxiedClient
|
|
PFWD_ :: CommandTag ProxiedClient
|
|
RFWD_ :: CommandTag ProxyService
|
|
NSUB_ :: CommandTag Notifier
|
|
NSUBS_ :: CommandTag NotifierService
|
|
RSLV_ :: CommandTag Resolver
|
|
|
|
data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p)
|
|
|
|
deriving instance Show (CommandTag p)
|
|
|
|
deriving instance Show CmdTag
|
|
|
|
data BrokerMsgTag
|
|
= IDS_
|
|
| LNK_
|
|
| SOK_
|
|
| SOKS_
|
|
| MSG_
|
|
| ALLS_
|
|
| NID_
|
|
| NMSG_
|
|
| PKEY_
|
|
| RRES_
|
|
| PRES_
|
|
| END_
|
|
| ENDS_
|
|
| DELD_
|
|
| INFO_
|
|
| OK_
|
|
| ERR_
|
|
| PONG_
|
|
| RNAME_
|
|
deriving (Show)
|
|
|
|
class ProtocolMsgTag t where
|
|
decodeTag :: ByteString -> Maybe t
|
|
|
|
messageTagP :: ProtocolMsgTag t => Parser t
|
|
messageTagP =
|
|
maybe (fail "bad message") pure . decodeTag
|
|
=<< (A.takeTill (== ' ') <* optional A.space)
|
|
|
|
instance PartyI p => Encoding (CommandTag p) where
|
|
smpEncode = \case
|
|
NEW_ -> "NEW"
|
|
SUB_ -> "SUB"
|
|
SUBS_ -> "SUBS"
|
|
KEY_ -> "KEY"
|
|
RKEY_ -> "RKEY"
|
|
LSET_ -> "LSET"
|
|
LDEL_ -> "LDEL"
|
|
NKEY_ -> "NKEY"
|
|
NDEL_ -> "NDEL"
|
|
GET_ -> "GET"
|
|
ACK_ -> "ACK"
|
|
OFF_ -> "OFF"
|
|
DEL_ -> "DEL"
|
|
QUE_ -> "QUE"
|
|
SKEY_ -> "SKEY"
|
|
SEND_ -> "SEND"
|
|
PING_ -> "PING"
|
|
LKEY_ -> "LKEY"
|
|
LGET_ -> "LGET"
|
|
PRXY_ -> "PRXY"
|
|
PFWD_ -> "PFWD"
|
|
RFWD_ -> "RFWD"
|
|
NSUB_ -> "NSUB"
|
|
NSUBS_ -> "NSUBS"
|
|
RSLV_ -> "RSLV"
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag CmdTag where
|
|
decodeTag = \case
|
|
"NEW" -> Just $ CT SCreator NEW_
|
|
"SUB" -> Just $ CT SRecipient SUB_
|
|
"SUBS" -> Just $ CT SRecipientService SUBS_
|
|
"KEY" -> Just $ CT SRecipient KEY_
|
|
"RKEY" -> Just $ CT SRecipient RKEY_
|
|
"LSET" -> Just $ CT SRecipient LSET_
|
|
"LDEL" -> Just $ CT SRecipient LDEL_
|
|
"NKEY" -> Just $ CT SRecipient NKEY_
|
|
"NDEL" -> Just $ CT SRecipient NDEL_
|
|
"GET" -> Just $ CT SRecipient GET_
|
|
"ACK" -> Just $ CT SRecipient ACK_
|
|
"OFF" -> Just $ CT SRecipient OFF_
|
|
"DEL" -> Just $ CT SRecipient DEL_
|
|
"QUE" -> Just $ CT SRecipient QUE_
|
|
"SKEY" -> Just $ CT SSender SKEY_
|
|
"SEND" -> Just $ CT SSender SEND_
|
|
"PING" -> Just $ CT SIdleClient PING_
|
|
"LKEY" -> Just $ CT SSenderLink LKEY_
|
|
"LGET" -> Just $ CT SSenderLink LGET_
|
|
"PRXY" -> Just $ CT SProxiedClient PRXY_
|
|
"PFWD" -> Just $ CT SProxiedClient PFWD_
|
|
"RFWD" -> Just $ CT SProxyService RFWD_
|
|
"NSUB" -> Just $ CT SNotifier NSUB_
|
|
"NSUBS" -> Just $ CT SNotifierService NSUBS_
|
|
"RSLV" -> Just $ CT SResolver RSLV_
|
|
_ -> Nothing
|
|
|
|
instance Encoding CmdTag where
|
|
smpEncode (CT _ t) = smpEncode t
|
|
smpP = messageTagP
|
|
|
|
instance PartyI p => ProtocolMsgTag (CommandTag p) where
|
|
decodeTag s = decodeTag s >>= (\(CT _ t) -> checkParty' t)
|
|
|
|
instance Encoding BrokerMsgTag where
|
|
smpEncode = \case
|
|
IDS_ -> "IDS"
|
|
LNK_ -> "LNK"
|
|
SOK_ -> "SOK"
|
|
SOKS_ -> "SOKS"
|
|
MSG_ -> "MSG"
|
|
ALLS_ -> "ALLS"
|
|
NID_ -> "NID"
|
|
NMSG_ -> "NMSG"
|
|
PKEY_ -> "PKEY"
|
|
RRES_ -> "RRES"
|
|
PRES_ -> "PRES"
|
|
END_ -> "END"
|
|
ENDS_ -> "ENDS"
|
|
DELD_ -> "DELD"
|
|
INFO_ -> "INFO"
|
|
OK_ -> "OK"
|
|
ERR_ -> "ERR"
|
|
PONG_ -> "PONG"
|
|
RNAME_ -> "RNAME"
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag BrokerMsgTag where
|
|
decodeTag = \case
|
|
"IDS" -> Just IDS_
|
|
"LNK" -> Just LNK_
|
|
"SOK" -> Just SOK_
|
|
"SOKS" -> Just SOKS_
|
|
"MSG" -> Just MSG_
|
|
"ALLS" -> Just ALLS_
|
|
"NID" -> Just NID_
|
|
"NMSG" -> Just NMSG_
|
|
"PKEY" -> Just PKEY_
|
|
"RRES" -> Just RRES_
|
|
"PRES" -> Just PRES_
|
|
"END" -> Just END_
|
|
"ENDS" -> Just ENDS_
|
|
"DELD" -> Just DELD_
|
|
"INFO" -> Just INFO_
|
|
"OK" -> Just OK_
|
|
"ERR" -> Just ERR_
|
|
"PONG" -> Just PONG_
|
|
"RNAME" -> Just RNAME_
|
|
_ -> Nothing
|
|
|
|
-- | SMP message body format
|
|
data ClientMsgEnvelope = ClientMsgEnvelope
|
|
{ cmHeader :: PubHeader,
|
|
cmNonce :: C.CbNonce,
|
|
cmEncBody :: ByteString
|
|
}
|
|
deriving (Show)
|
|
|
|
data PubHeader = PubHeader
|
|
{ phVersion :: VersionSMPC,
|
|
phE2ePubDhKey :: Maybe C.PublicKeyX25519
|
|
}
|
|
deriving (Show)
|
|
|
|
instance Encoding PubHeader where
|
|
smpEncode (PubHeader v k) = smpEncode (v, k)
|
|
smpP = PubHeader <$> smpP <*> smpP
|
|
|
|
instance Encoding ClientMsgEnvelope where
|
|
smpEncode ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} =
|
|
smpEncode (cmHeader, cmNonce, Tail cmEncBody)
|
|
smpP = do
|
|
(cmHeader, cmNonce, Tail cmEncBody) <- smpP
|
|
pure ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
|
|
|
|
data ClientMessage = ClientMessage PrivHeader ByteString
|
|
|
|
data PrivHeader
|
|
= PHConfirmation C.APublicAuthKey
|
|
| PHEmpty
|
|
deriving (Show)
|
|
|
|
instance Encoding PrivHeader where
|
|
smpEncode = \case
|
|
PHConfirmation k -> "K" <> smpEncode k
|
|
PHEmpty -> "_"
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'K' -> PHConfirmation <$> smpP
|
|
'_' -> pure PHEmpty
|
|
_ -> fail "invalid PrivHeader"
|
|
|
|
instance Encoding ClientMessage where
|
|
smpEncode (ClientMessage h msg) = smpEncode h <> msg
|
|
smpP = ClientMessage <$> smpP <*> A.takeByteString
|
|
|
|
type SMPServer = ProtocolServer 'PSMP
|
|
|
|
pattern SMPServer :: NonEmpty TransportHost -> ServiceName -> C.KeyHash -> ProtocolServer 'PSMP
|
|
pattern SMPServer host port keyHash = ProtocolServer SPSMP host port keyHash
|
|
|
|
{-# COMPLETE SMPServer #-}
|
|
|
|
type SMPServerWithAuth = ProtoServerWithAuth 'PSMP
|
|
|
|
type NtfServer = ProtocolServer 'PNTF
|
|
|
|
pattern NtfServer :: NonEmpty TransportHost -> ServiceName -> C.KeyHash -> ProtocolServer 'PNTF
|
|
pattern NtfServer host port keyHash = ProtocolServer SPNTF host port keyHash
|
|
|
|
{-# COMPLETE NtfServer #-}
|
|
|
|
type NtfServerWithAuth = ProtoServerWithAuth 'PNTF
|
|
|
|
type XFTPServer = ProtocolServer 'PXFTP
|
|
|
|
pattern XFTPServer :: NonEmpty TransportHost -> ServiceName -> C.KeyHash -> ProtocolServer 'PXFTP
|
|
pattern XFTPServer host port keyHash = ProtocolServer SPXFTP host port keyHash
|
|
|
|
{-# COMPLETE XFTPServer #-}
|
|
|
|
type XFTPServerWithAuth = ProtoServerWithAuth 'PXFTP
|
|
|
|
sameSrvAddr' :: ProtoServerWithAuth p -> ProtoServerWithAuth p -> Bool
|
|
sameSrvAddr' (ProtoServerWithAuth srv _) (ProtoServerWithAuth srv' _) = sameSrvAddr srv srv'
|
|
{-# INLINE sameSrvAddr' #-}
|
|
|
|
sameSrvAddr :: ProtocolServer p -> ProtocolServer p -> Bool
|
|
sameSrvAddr ProtocolServer {host, port} ProtocolServer {host = h', port = p'} = host == h' && port == p'
|
|
{-# INLINE sameSrvAddr #-}
|
|
|
|
data ProtocolType = PSMP | PNTF | PXFTP
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance StrEncoding ProtocolType where
|
|
strEncode = \case
|
|
PSMP -> "smp"
|
|
PNTF -> "ntf"
|
|
PXFTP -> "xftp"
|
|
strP =
|
|
A.takeTill (\c -> c == ':' || c == ' ') >>= \case
|
|
"smp" -> pure PSMP
|
|
"ntf" -> pure PNTF
|
|
"xftp" -> pure PXFTP
|
|
_ -> fail "bad ProtocolType"
|
|
|
|
data SProtocolType (p :: ProtocolType) where
|
|
SPSMP :: SProtocolType 'PSMP
|
|
SPNTF :: SProtocolType 'PNTF
|
|
SPXFTP :: SProtocolType 'PXFTP
|
|
|
|
deriving instance Eq (SProtocolType p)
|
|
|
|
deriving instance Ord (SProtocolType p)
|
|
|
|
deriving instance Show (SProtocolType p)
|
|
|
|
data AProtocolType = forall p. ProtocolTypeI p => AProtocolType (SProtocolType p)
|
|
|
|
instance Eq AProtocolType where
|
|
AProtocolType p == AProtocolType p' = isJust $ testEquality p p'
|
|
|
|
deriving instance Show AProtocolType
|
|
|
|
instance TestEquality SProtocolType where
|
|
testEquality SPSMP SPSMP = Just Refl
|
|
testEquality SPNTF SPNTF = Just Refl
|
|
testEquality SPXFTP SPXFTP = Just Refl
|
|
testEquality _ _ = Nothing
|
|
|
|
protocolType :: SProtocolType p -> ProtocolType
|
|
protocolType = \case
|
|
SPSMP -> PSMP
|
|
SPNTF -> PNTF
|
|
SPXFTP -> PXFTP
|
|
|
|
aProtocolType :: ProtocolType -> AProtocolType
|
|
aProtocolType = \case
|
|
PSMP -> AProtocolType SPSMP
|
|
PNTF -> AProtocolType SPNTF
|
|
PXFTP -> AProtocolType SPXFTP
|
|
|
|
instance ProtocolTypeI p => StrEncoding (SProtocolType p) where
|
|
strEncode = strEncode . protocolType
|
|
strP = (\(AProtocolType p) -> checkProtocolType p) <$?> strP
|
|
|
|
instance StrEncoding AProtocolType where
|
|
strEncode (AProtocolType p) = strEncode p
|
|
strP = aProtocolType <$> strP
|
|
|
|
instance ProtocolTypeI p => ToJSON (SProtocolType p) where
|
|
toEncoding = strToJEncoding
|
|
toJSON = strToJSON
|
|
|
|
instance ProtocolTypeI p => FromJSON (SProtocolType p) where
|
|
parseJSON = strParseJSON "SProtocolType"
|
|
|
|
instance ToJSON AProtocolType where
|
|
toEncoding = strToJEncoding
|
|
toJSON = strToJSON
|
|
|
|
instance FromJSON AProtocolType where
|
|
parseJSON = strParseJSON "AProtocolType"
|
|
|
|
checkProtocolType :: forall t p p'. (ProtocolTypeI p, ProtocolTypeI p') => t p' -> Either String (t p)
|
|
checkProtocolType p = case testEquality (protocolTypeI @p) (protocolTypeI @p') of
|
|
Just Refl -> Right p
|
|
Nothing -> Left "bad ProtocolType"
|
|
|
|
class ProtocolTypeI (p :: ProtocolType) where
|
|
protocolTypeI :: SProtocolType p
|
|
|
|
instance ProtocolTypeI 'PSMP where protocolTypeI = SPSMP
|
|
|
|
instance ProtocolTypeI 'PNTF where protocolTypeI = SPNTF
|
|
|
|
instance ProtocolTypeI 'PXFTP where protocolTypeI = SPXFTP
|
|
|
|
type family UserProtocol (p :: ProtocolType) :: Constraint where
|
|
UserProtocol PSMP = ()
|
|
UserProtocol PXFTP = ()
|
|
UserProtocol a =
|
|
(Int ~ Bool, TypeError (TE.Text "Servers for protocol " :<>: ShowType a :<>: TE.Text " cannot be configured by the users"))
|
|
|
|
userProtocol :: SProtocolType p -> Maybe (Dict (UserProtocol p))
|
|
userProtocol = \case
|
|
SPSMP -> Just Dict
|
|
SPXFTP -> Just Dict
|
|
_ -> Nothing
|
|
|
|
-- | server location and transport key digest (hash).
|
|
data ProtocolServer p = ProtocolServer
|
|
{ scheme :: SProtocolType p,
|
|
host :: NonEmpty TransportHost,
|
|
port :: ServiceName,
|
|
keyHash :: C.KeyHash
|
|
}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
data AProtocolServer = forall p. ProtocolTypeI p => AProtocolServer (SProtocolType p) (ProtocolServer p)
|
|
|
|
instance ProtocolTypeI p => IsString (ProtocolServer p) where
|
|
fromString = parseString strDecode
|
|
|
|
instance ProtocolTypeI p => Encoding (ProtocolServer p) where
|
|
smpEncode ProtocolServer {host, port, keyHash} =
|
|
smpEncode (host, port, keyHash)
|
|
smpP = do
|
|
(host, port, keyHash) <- smpP
|
|
pure ProtocolServer {scheme = protocolTypeI @p, host, port, keyHash}
|
|
|
|
instance ProtocolTypeI p => StrEncoding (ProtocolServer p) where
|
|
strEncode ProtocolServer {scheme, host, port, keyHash} =
|
|
strEncodeServer scheme (strEncode host) port keyHash Nothing
|
|
strP =
|
|
serverStrP >>= \case
|
|
(AProtocolServer _ srv, Nothing) -> either fail pure $ checkProtocolType srv
|
|
_ -> fail "ProtocolServer with basic auth not allowed"
|
|
|
|
instance ProtocolTypeI p => ToJSON (ProtocolServer p) where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
instance ProtocolTypeI p => FromJSON (ProtocolServer p) where
|
|
parseJSON = strParseJSON "ProtocolServer"
|
|
|
|
newtype BasicAuth = BasicAuth {unBasicAuth :: ByteString}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance IsString BasicAuth where fromString = BasicAuth . B.pack
|
|
|
|
instance Encoding BasicAuth where
|
|
smpEncode (BasicAuth s) = smpEncode s
|
|
smpP = basicAuth <$?> smpP
|
|
|
|
instance StrEncoding BasicAuth where
|
|
strEncode (BasicAuth s) = s
|
|
strP = basicAuth <$?> A.takeWhile1 (/= '@')
|
|
|
|
basicAuth :: ByteString -> Either String BasicAuth
|
|
basicAuth s
|
|
| B.all valid s = Right $ BasicAuth s
|
|
| otherwise = Left "invalid character in BasicAuth"
|
|
where
|
|
valid c = isPrint c && not (isSpace c) && c /= '@' && c /= ':' && c /= '/'
|
|
|
|
data ProtoServerWithAuth p = ProtoServerWithAuth {protoServer :: ProtocolServer p, serverBasicAuth :: Maybe BasicAuth}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance ProtocolTypeI p => IsString (ProtoServerWithAuth p) where
|
|
fromString = parseString strDecode
|
|
|
|
data AProtoServerWithAuth = forall p. ProtocolTypeI p => AProtoServerWithAuth (SProtocolType p) (ProtoServerWithAuth p)
|
|
|
|
deriving instance Show AProtoServerWithAuth
|
|
|
|
instance ProtocolTypeI p => StrEncoding (ProtoServerWithAuth p) where
|
|
strEncode (ProtoServerWithAuth ProtocolServer {scheme, host, port, keyHash} auth_) =
|
|
strEncodeServer scheme (strEncode host) port keyHash auth_
|
|
strP = (\(AProtoServerWithAuth _ srv) -> checkProtocolType srv) <$?> strP
|
|
|
|
instance StrEncoding AProtoServerWithAuth where
|
|
strEncode (AProtoServerWithAuth _ srv) = strEncode srv
|
|
strP =
|
|
serverStrP >>= \(AProtocolServer p srv, auth) ->
|
|
pure $ AProtoServerWithAuth p (ProtoServerWithAuth srv auth)
|
|
|
|
instance ProtocolTypeI p => ToJSON (ProtoServerWithAuth p) where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
instance ProtocolTypeI p => FromJSON (ProtoServerWithAuth p) where
|
|
parseJSON = strParseJSON "ProtoServerWithAuth"
|
|
|
|
instance ToJSON AProtoServerWithAuth where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
instance FromJSON AProtoServerWithAuth where
|
|
parseJSON = strParseJSON "AProtoServerWithAuth"
|
|
|
|
noAuthSrv :: ProtocolServer p -> ProtoServerWithAuth p
|
|
noAuthSrv srv = ProtoServerWithAuth srv Nothing
|
|
|
|
legacyEncodeServer :: ProtocolServer p -> ByteString
|
|
legacyEncodeServer ProtocolServer {host, port, keyHash} =
|
|
smpEncode (L.head host, port, keyHash)
|
|
|
|
legacyServerP :: forall p. ProtocolTypeI p => Parser (ProtocolServer p)
|
|
legacyServerP = do
|
|
(h, port, keyHash) <- smpP
|
|
pure ProtocolServer {scheme = protocolTypeI @p, host = [h], port, keyHash}
|
|
|
|
legacyStrEncodeServer :: ProtocolTypeI p => ProtocolServer p -> ByteString
|
|
legacyStrEncodeServer ProtocolServer {scheme, host, port, keyHash} =
|
|
strEncodeServer scheme (strEncode $ L.head host) port keyHash Nothing
|
|
|
|
strEncodeServer :: ProtocolTypeI p => SProtocolType p -> ByteString -> ServiceName -> C.KeyHash -> Maybe BasicAuth -> ByteString
|
|
strEncodeServer scheme host port keyHash auth_ =
|
|
strEncode scheme <> "://" <> strEncode keyHash <> maybe "" ((":" <>) . strEncode) auth_ <> "@" <> host <> portStr
|
|
where
|
|
portStr = B.pack $ if null port then "" else ':' : port
|
|
|
|
serverStrP :: Parser (AProtocolServer, Maybe BasicAuth)
|
|
serverStrP = do
|
|
scheme <- strP <* "://"
|
|
keyHash <- strP
|
|
auth_ <- optional $ A.char ':' *> strP
|
|
TransportHosts host <- A.char '@' *> strP
|
|
port <- portP <|> pure ""
|
|
pure $ case scheme of
|
|
AProtocolType s -> (AProtocolServer s $ ProtocolServer {scheme = s, host, port, keyHash}, auth_)
|
|
where
|
|
portP = show <$> (A.char ':' *> (A.decimal :: Parser Int))
|
|
|
|
-- | Transmission correlation ID.
|
|
newtype CorrId = CorrId {bs :: ByteString}
|
|
deriving (Eq, Ord, Show)
|
|
deriving newtype (Encoding)
|
|
|
|
pattern NoCorrId :: CorrId
|
|
pattern NoCorrId = CorrId ""
|
|
|
|
instance IsString CorrId where
|
|
fromString = CorrId . fromString
|
|
{-# INLINE fromString #-}
|
|
|
|
instance StrEncoding CorrId where
|
|
strEncode (CorrId cId) = strEncode cId
|
|
strDecode s = CorrId <$> strDecode s
|
|
strP = CorrId <$> strP
|
|
|
|
instance ToJSON CorrId where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
instance FromJSON CorrId where
|
|
parseJSON = strParseJSON "CorrId"
|
|
|
|
-- | Queue IDs and keys
|
|
data QueueIdsKeys = QIK
|
|
{ rcvId :: RecipientId,
|
|
sndId :: SenderId,
|
|
rcvPublicDhKey :: RcvPublicDhKey,
|
|
queueMode :: Maybe QueueMode, -- TODO remove Maybe when min version is 9 (sndAuthKeySMPVersion)
|
|
linkId :: Maybe LinkId,
|
|
serviceId :: Maybe ServiceId,
|
|
serverNtfCreds :: Maybe ServerNtfCreds
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey
|
|
deriving (Eq, Show)
|
|
|
|
instance Encoding ServerNtfCreds where
|
|
smpEncode (ServerNtfCreds nId dhKey) = smpEncode (nId, dhKey)
|
|
smpP = ServerNtfCreds <$> smpP <*> smpP
|
|
|
|
-- | Recipient's private key used by the recipient to authorize (v6: sign, v7: encrypt hash) SMP commands.
|
|
--
|
|
-- Only used by SMP agent, kept here so its definition is close to respective public key.
|
|
type RcvPrivateAuthKey = C.APrivateAuthKey
|
|
|
|
-- | Recipient's public key used by SMP server to verify authorization of SMP commands.
|
|
type RcvPublicAuthKey = C.APublicAuthKey
|
|
|
|
-- | Public key used for DH exchange to encrypt message bodies from server to recipient
|
|
type RcvPublicDhKey = C.PublicKeyX25519
|
|
|
|
-- | DH Secret used to encrypt message bodies from server to recipient
|
|
type RcvDhSecret = C.DhSecretX25519
|
|
|
|
-- | Sender's private key used by the recipient to authorize (v6: sign, v7: encrypt hash) SMP commands.
|
|
--
|
|
-- Only used by SMP agent, kept here so its definition is close to respective public key.
|
|
type SndPrivateAuthKey = C.APrivateAuthKey
|
|
|
|
-- | Sender's public key used by SMP server to verify authorization of SMP commands.
|
|
type SndPublicAuthKey = C.APublicAuthKey
|
|
|
|
-- | Private key used by push notifications server to authorize (sign or encrypt hash) NSUB command.
|
|
type NtfPrivateAuthKey = C.APrivateAuthKey
|
|
|
|
-- | Public key used by SMP server to verify authorization of NSUB command sent by push notifications server.
|
|
type NtfPublicAuthKey = C.APublicAuthKey
|
|
|
|
-- | Public key used for DH exchange to encrypt notification metadata from server to recipient
|
|
type RcvNtfPublicDhKey = C.PublicKeyX25519
|
|
|
|
-- | DH Secret used to encrypt notification metadata from server to recipient
|
|
type RcvNtfDhSecret = C.DhSecretX25519
|
|
|
|
-- | SMP message server ID.
|
|
type MsgId = ByteString
|
|
|
|
-- | SMP message body.
|
|
type MsgBody = ByteString
|
|
|
|
data ServiceSub = ServiceSub
|
|
{ smpServiceId :: ServiceId,
|
|
smpQueueCount :: Int64,
|
|
smpQueueIdsHash :: IdsHash
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data ServiceSubResult = ServiceSubResult (Maybe ServiceSubError) ServiceSub
|
|
deriving (Eq, Show)
|
|
|
|
data ServiceSubError
|
|
= SSErrorServiceId {expectedServiceId :: ServiceId, subscribedServiceId :: ServiceId}
|
|
| SSErrorQueueCount {expectedQueueCount :: Int64, subscribedQueueCount :: Int64}
|
|
| SSErrorQueueIdsHash {expectedQueueIdsHash :: IdsHash, subscribedQueueIdsHash :: IdsHash}
|
|
deriving (Eq, Show)
|
|
|
|
serviceSubResult :: ServiceSub -> ServiceSub -> ServiceSubResult
|
|
serviceSubResult s s' = ServiceSubResult subError_ s'
|
|
where
|
|
subError_
|
|
| smpServiceId s /= smpServiceId s' = Just $ SSErrorServiceId (smpServiceId s) (smpServiceId s')
|
|
| smpQueueCount s /= smpQueueCount s' = Just $ SSErrorQueueCount (smpQueueCount s) (smpQueueCount s')
|
|
| smpQueueIdsHash s /= smpQueueIdsHash s' = Just $ SSErrorQueueIdsHash (smpQueueIdsHash s) (smpQueueIdsHash s')
|
|
| otherwise = Nothing
|
|
|
|
newtype IdsHash = IdsHash {unIdsHash :: BS.ByteString}
|
|
deriving (Eq, Show)
|
|
deriving newtype (Encoding, FromField)
|
|
|
|
instance ToField IdsHash where
|
|
toField (IdsHash s) = toField (Binary s)
|
|
{-# INLINE toField #-}
|
|
|
|
instance Semigroup IdsHash where
|
|
(IdsHash s1) <> (IdsHash s2) = IdsHash $! BS.pack $ BS.zipWith xor s1 s2
|
|
|
|
instance Monoid IdsHash where
|
|
mempty = IdsHash $ BS.replicate 16 0
|
|
mconcat ss =
|
|
let !s' = BS.pack $ foldl' (\ !r (IdsHash s) -> zipWith xor' r (BS.unpack s)) (replicate 16 0) ss -- to prevent packing/unpacking in <> on each step with default mappend
|
|
in IdsHash s'
|
|
|
|
xor' :: Word8 -> Word8 -> Word8
|
|
xor' x y = let !r = xor x y in r
|
|
|
|
queueIdsHash :: [QueueId] -> IdsHash
|
|
queueIdsHash = mconcat . map queueIdHash
|
|
|
|
queueIdHash :: QueueId -> IdsHash
|
|
queueIdHash = IdsHash . C.md5Hash . unEntityId
|
|
{-# INLINE queueIdHash #-}
|
|
|
|
addServiceSubs :: (Int64, IdsHash) -> (Int64, IdsHash) -> (Int64, IdsHash)
|
|
addServiceSubs (n', idsHash') (n, idsHash) = (n + n', idsHash <> idsHash')
|
|
|
|
subtractServiceSubs :: (Int64, IdsHash) -> (Int64, IdsHash) -> (Int64, IdsHash)
|
|
subtractServiceSubs (n', idsHash') (n, idsHash)
|
|
| n > n' = (n - n', idsHash <> idsHash') -- concat is a reversible xor: (x `xor` y) `xor` y == x
|
|
| otherwise = (0, mempty)
|
|
|
|
data ProtocolErrorType = PECmdSyntax | PECmdUnknown | PESession | PEBlock
|
|
|
|
-- | Type for protocol errors.
|
|
data ErrorType
|
|
= -- | incorrect block format, encoding or signature size
|
|
BLOCK
|
|
| -- | incorrect SMP session ID (TLS Finished message / tls-unique binding RFC5929)
|
|
SESSION
|
|
| -- | SMP command is unknown or has invalid syntax
|
|
CMD {cmdErr :: CommandError}
|
|
| -- | error from proxied relay
|
|
PROXY {proxyErr :: ProxyError}
|
|
| -- | command authorization error - bad signature or non-existing SMP queue
|
|
AUTH
|
|
| -- | command with the entity that was blocked
|
|
BLOCKED {blockInfo :: BlockingInfo}
|
|
| -- | service signature is not allowed for command or session; service command is sent not in service session
|
|
SERVICE
|
|
| -- | encryption/decryption error in proxy protocol
|
|
CRYPTO
|
|
| -- | SMP queue capacity is exceeded on the server
|
|
QUOTA
|
|
| -- | SMP server storage error
|
|
STORE {storeErr :: Text}
|
|
| -- | ACK command is sent without message to be acknowledged
|
|
NO_MSG
|
|
| -- | sent message is too large (> maxMessageLength = 16088 bytes)
|
|
LARGE_MSG
|
|
| -- | relay public key is expired
|
|
EXPIRED
|
|
| -- | internal server error
|
|
INTERNAL
|
|
| -- | name resolution error
|
|
NAME {nameErr :: NameErrorType}
|
|
| -- | used internally, never returned by the server (to be removed)
|
|
DUPLICATE_ -- not part of SMP protocol, used internally
|
|
deriving (Eq, Show)
|
|
|
|
-- | Name resolution error
|
|
data NameErrorType
|
|
= -- | the names role / resolver is not configured on this server
|
|
NO_RESOLVER
|
|
| -- | the name is not registered (resolver returned not-found)
|
|
NOT_FOUND
|
|
| -- | backing resolver/RPC failure - contains the diagnostic detail
|
|
RESOLVER {resolverErr :: Text}
|
|
deriving (Eq, Show)
|
|
|
|
instance StrEncoding ErrorType where
|
|
strEncode = \case
|
|
BLOCK -> "BLOCK"
|
|
SESSION -> "SESSION"
|
|
CMD e -> "CMD " <> bshow e
|
|
PROXY e -> "PROXY " <> strEncode e
|
|
AUTH -> "AUTH"
|
|
BLOCKED info -> "BLOCKED " <> strEncode info
|
|
SERVICE -> "SERVICE"
|
|
CRYPTO -> "CRYPTO"
|
|
QUOTA -> "QUOTA"
|
|
STORE e -> "STORE " <> encodeUtf8 e
|
|
NO_MSG -> "NO_MSG"
|
|
LARGE_MSG -> "LARGE_MSG"
|
|
EXPIRED -> "EXPIRED"
|
|
INTERNAL -> "INTERNAL"
|
|
NAME e -> "NAME " <> strEncode e
|
|
DUPLICATE_ -> "DUPLICATE_"
|
|
strP =
|
|
A.choice
|
|
[ "BLOCK" $> BLOCK,
|
|
"SESSION" $> SESSION,
|
|
"CMD " *> (CMD <$> parseRead1),
|
|
"PROXY " *> (PROXY <$> strP),
|
|
"NAME " *> (NAME <$> strP),
|
|
"AUTH" $> AUTH,
|
|
"BLOCKED " *> strP,
|
|
"SERVICE" $> SERVICE,
|
|
"CRYPTO" $> CRYPTO,
|
|
"QUOTA" $> QUOTA,
|
|
"STORE " *> (STORE . safeDecodeUtf8 <$> A.takeByteString),
|
|
"NO_MSG" $> NO_MSG,
|
|
"LARGE_MSG" $> LARGE_MSG,
|
|
"EXPIRED" $> EXPIRED,
|
|
"INTERNAL" $> INTERNAL,
|
|
"DUPLICATE_" $> DUPLICATE_
|
|
]
|
|
|
|
-- | SMP command error type.
|
|
data CommandError
|
|
= -- | unknown command
|
|
UNKNOWN
|
|
| -- | error parsing command
|
|
SYNTAX
|
|
| -- | command is not allowed (bad service role, or SUB/GET used with the same queue in the same TCP session)
|
|
PROHIBITED
|
|
| -- | transmission has no required credentials (signature or queue ID)
|
|
NO_AUTH
|
|
| -- | transmission has credentials that are not allowed for this command
|
|
HAS_AUTH
|
|
| -- | transmission has no required entity ID (e.g. SMP queue)
|
|
NO_ENTITY
|
|
deriving (Eq, Read, Show)
|
|
|
|
data ProxyError
|
|
= -- | Correctly parsed SMP server ERR response.
|
|
-- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`.
|
|
PROTOCOL {protocolErr :: ErrorType}
|
|
| -- | destination server error
|
|
BROKER {brokerErr :: BrokerErrorType}
|
|
| -- | basic auth provided to proxy is invalid
|
|
BASIC_AUTH
|
|
| -- no destination server error
|
|
NO_SESSION
|
|
deriving (Eq, Show)
|
|
|
|
-- | SMP server errors.
|
|
data BrokerErrorType
|
|
= -- | invalid server response (failed to parse)
|
|
RESPONSE {respErr :: String}
|
|
| -- | unexpected response
|
|
UNEXPECTED {respErr :: String}
|
|
| -- | network error
|
|
NETWORK {networkError :: NetworkError}
|
|
| -- | no compatible server host (e.g. onion when public is required, or vice versa)
|
|
HOST
|
|
| -- | service unavailable client-side - used in agent errors
|
|
NO_SERVICE
|
|
| -- | handshake or other transport error
|
|
TRANSPORT {transportErr :: TransportError}
|
|
| -- | command response timeout
|
|
TIMEOUT
|
|
deriving (Eq, Read, Show, Exception)
|
|
|
|
data NetworkError
|
|
= NEConnectError {connectError :: String}
|
|
| NETLSError {tlsError :: String}
|
|
| NEUnknownCAError
|
|
| NEFailedError
|
|
| NETimeoutError
|
|
| NESubscribeError {subscribeError :: String}
|
|
deriving (Eq, Read, Show)
|
|
|
|
toNetworkError :: SomeException -> NetworkError
|
|
toNetworkError e = maybe (NEConnectError err) fromTLSError (fromException e)
|
|
where
|
|
err = displayException e
|
|
fromTLSError :: TLS.TLSException -> NetworkError
|
|
fromTLSError = \case
|
|
TLS.HandshakeFailed (TLS.Error_Protocol _ TLS.UnknownCa) -> NEUnknownCAError
|
|
_ -> NETLSError err
|
|
|
|
data BlockingInfo = BlockingInfo
|
|
{ reason :: BlockingReason,
|
|
notice :: Maybe ClientNotice
|
|
}
|
|
deriving (Eq, Show)
|
|
|
|
data BlockingReason = BRSpam | BRContent
|
|
deriving (Eq, Show)
|
|
|
|
instance StrEncoding BlockingInfo where
|
|
strEncode BlockingInfo {reason, notice} =
|
|
"reason=" <> strEncode reason <> maybe "" ((",notice=" <>) . LB.toStrict . J.encode) notice
|
|
strP = do
|
|
reason <- "reason=" *> strP
|
|
notice <- optional $ ",notice=" *> (J.eitherDecodeStrict <$?> A.takeByteString)
|
|
pure BlockingInfo {reason, notice}
|
|
|
|
instance Encoding BlockingInfo where
|
|
smpEncode = strEncode
|
|
smpP = strP
|
|
|
|
instance StrEncoding BlockingReason where
|
|
strEncode = \case
|
|
BRSpam -> "spam"
|
|
BRContent -> "content"
|
|
strP = "spam" $> BRSpam <|> "content" $> BRContent
|
|
|
|
instance ToJSON BlockingReason where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
instance FromJSON BlockingReason where
|
|
parseJSON = strParseJSON "BlockingReason"
|
|
|
|
-- | SMP transmission parser.
|
|
transmissionP :: THandleParams v p -> Parser RawTransmission
|
|
transmissionP THandleParams {sessionId, implySessId, serviceAuth} = do
|
|
authenticator <- smpP
|
|
serviceSig <- if serviceAuth && not (B.null authenticator) then smpP else pure Nothing
|
|
authorized <- A.takeByteString
|
|
either fail pure $ parseAll (trn authenticator serviceSig authorized) authorized
|
|
where
|
|
trn authenticator serviceSig authorized = do
|
|
sessId <- if implySessId then pure "" else smpP
|
|
let authorized' = if implySessId then smpEncode sessionId <> authorized else authorized
|
|
corrId <- smpP
|
|
entityId <- smpP
|
|
command <- A.takeByteString
|
|
pure RawTransmission {authenticator, serviceSig, authorized = authorized', sessId, corrId, entityId, command}
|
|
|
|
class (ProtocolTypeI (ProtoType msg), ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where
|
|
type ProtoCommand msg = cmd | cmd -> msg
|
|
type ProtoType msg = (sch :: ProtocolType) | sch -> msg
|
|
protocolClientHandshake :: Transport c => c 'TClient -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> Bool -> Maybe (ServiceCredentials, C.KeyPairEd25519) -> ExceptT TransportError IO (THandle v c 'TClient)
|
|
useServiceAuth :: ProtoCommand msg -> Bool
|
|
protocolPing :: ProtoCommand msg
|
|
protocolError :: msg -> Maybe err
|
|
|
|
type ProtoServer msg = ProtocolServer (ProtoType msg)
|
|
|
|
instance Protocol SMPVersion ErrorType BrokerMsg where
|
|
type ProtoCommand BrokerMsg = Cmd
|
|
type ProtoType BrokerMsg = 'PSMP
|
|
protocolClientHandshake = smpClientHandshake
|
|
{-# INLINE protocolClientHandshake #-}
|
|
useServiceAuth = \case
|
|
Cmd _ (NEW _) -> True
|
|
Cmd _ SUB -> True
|
|
Cmd _ NSUB -> True
|
|
_ -> False
|
|
{-# INLINE useServiceAuth #-}
|
|
protocolPing = Cmd SIdleClient PING
|
|
{-# INLINE protocolPing #-}
|
|
protocolError = \case
|
|
ERR e -> Just e
|
|
_ -> Nothing
|
|
{-# INLINE protocolError #-}
|
|
|
|
class ProtocolMsgTag (Tag msg) => ProtocolEncoding v err msg | msg -> err, msg -> v where
|
|
type Tag msg
|
|
encodeProtocol :: Version v -> msg -> ByteString
|
|
protocolP :: Version v -> Tag msg -> Parser msg
|
|
fromProtocolError :: ProtocolErrorType -> err
|
|
checkCredentials :: Maybe TAuthorizations -> EntityId -> msg -> Either err msg
|
|
|
|
instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
|
type Tag (Command p) = CommandTag p
|
|
encodeProtocol v = \case
|
|
NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData, ntfCreds}
|
|
| v >= newNtfCredsSMPVersion -> new <> e (auth_, subMode, queueReqData, ntfCreds)
|
|
| v >= shortLinksSMPVersion -> new <> e (auth_, subMode, queueReqData)
|
|
| v >= sndAuthKeySMPVersion -> new <> e (auth_, subMode, senderCanSecure (queueReqMode <$> queueReqData))
|
|
| otherwise -> new <> auth <> e subMode
|
|
where
|
|
new = e (NEW_, ' ', rKey, dhKey)
|
|
auth = maybe "" (e . ('A',)) auth_
|
|
SUB -> e SUB_
|
|
SUBS n idsHash
|
|
| v >= rcvServiceSMPVersion -> e (SUBS_, ' ', n, idsHash)
|
|
| otherwise -> e SUBS_
|
|
KEY k -> e (KEY_, ' ', k)
|
|
RKEY ks -> e (RKEY_, ' ', ks)
|
|
LSET lnkId d -> e (LSET_, ' ', lnkId, d)
|
|
LDEL -> e LDEL_
|
|
NKEY k dhKey -> e (NKEY_, ' ', k, dhKey)
|
|
NDEL -> e NDEL_
|
|
GET -> e GET_
|
|
ACK msgId -> e (ACK_, ' ', msgId)
|
|
OFF -> e OFF_
|
|
DEL -> e DEL_
|
|
QUE -> e QUE_
|
|
SKEY k -> e (SKEY_, ' ', k)
|
|
SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg)
|
|
PING -> e PING_
|
|
NSUB -> e NSUB_
|
|
NSUBS n idsHash
|
|
| v >= rcvServiceSMPVersion -> e (NSUBS_, ' ', n, idsHash)
|
|
| otherwise -> e NSUBS_
|
|
LKEY k -> e (LKEY_, ' ', k)
|
|
LGET -> e LGET_
|
|
PRXY host auth_ -> e (PRXY_, ' ', host, auth_)
|
|
PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s)
|
|
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
|
|
RSLV d -> e (RSLV_, ' ', d)
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
|
|
protocolP v tag = (\(Cmd _ c) -> checkParty c) <$?> protocolP v (CT (sParty @p) tag)
|
|
|
|
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials auth (EntityId entId) cmd = case cmd of
|
|
-- NEW must have signature but NOT queue ID
|
|
NEW {}
|
|
| isNothing auth -> Left $ CMD NO_AUTH
|
|
| not (B.null entId) -> Left $ CMD HAS_AUTH
|
|
| otherwise -> Right cmd
|
|
-- SEND must have queue ID, signature is not always required
|
|
SEND {}
|
|
| B.null entId -> Left $ CMD NO_ENTITY
|
|
| otherwise -> Right cmd
|
|
LGET -> entityCmd
|
|
PING -> noAuthCmd
|
|
PRXY {} -> noAuthCmd
|
|
PFWD {} -> entityCmd
|
|
RFWD _ -> noAuthCmd
|
|
RSLV _ -> noAuthCmd
|
|
SUB -> serviceCmd
|
|
NSUB -> serviceCmd
|
|
-- other client commands must have both signature and queue ID
|
|
_
|
|
| isNothing auth || B.null entId -> Left $ CMD NO_AUTH
|
|
| otherwise -> Right cmd
|
|
where
|
|
-- command must not have entity ID (queue or session ID) or signature
|
|
noAuthCmd :: Either ErrorType (Command p)
|
|
noAuthCmd
|
|
| isNothing auth && B.null entId = Right cmd
|
|
| otherwise = Left $ CMD HAS_AUTH
|
|
entityCmd :: Either ErrorType (Command p)
|
|
entityCmd
|
|
| B.null entId = Left $ CMD NO_ENTITY
|
|
| isNothing auth = Right cmd
|
|
| otherwise = Left $ CMD HAS_AUTH
|
|
serviceCmd :: Either ErrorType (Command p)
|
|
serviceCmd
|
|
| isNothing auth || B.null entId = Left $ CMD NO_AUTH
|
|
| otherwise = Right cmd
|
|
|
|
instance ProtocolEncoding SMPVersion ErrorType Cmd where
|
|
type Tag Cmd = CmdTag
|
|
encodeProtocol v (Cmd _ c) = encodeProtocol v c
|
|
{-# INLINE encodeProtocol #-}
|
|
|
|
protocolP v = \case
|
|
CT SCreator NEW_ -> Cmd SCreator <$> newCmd
|
|
where
|
|
newCmd
|
|
| v >= newNtfCredsSMPVersion = new smpP smpP smpP
|
|
| v >= shortLinksSMPVersion = new smpP smpP nothing
|
|
| v >= sndAuthKeySMPVersion = new smpP (qReq <$> smpP) nothing
|
|
| otherwise = new auth nothing nothing
|
|
where
|
|
nothing = pure Nothing
|
|
new p1 p2 p3 = NEW <$> do
|
|
rcvAuthKey <- _smpP
|
|
rcvDhKey <- smpP
|
|
auth_ <- p1
|
|
subMode <- smpP
|
|
queueReqData <- p2
|
|
ntfCreds <- p3
|
|
pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData, ntfCreds}
|
|
auth = optional (A.char 'A' *> smpP)
|
|
qReq sndSecure = Just $ if sndSecure then QRMessaging Nothing else QRContact Nothing
|
|
CT SRecipient tag ->
|
|
Cmd SRecipient <$> case tag of
|
|
SUB_ -> pure SUB
|
|
KEY_ -> KEY <$> _smpP
|
|
RKEY_ -> RKEY <$> _smpP
|
|
LSET_ -> LSET <$> _smpP <*> smpP
|
|
LDEL_ -> pure LDEL
|
|
NKEY_ -> NKEY <$> _smpP <*> smpP
|
|
NDEL_ -> pure NDEL
|
|
GET_ -> pure GET
|
|
ACK_ -> ACK <$> _smpP
|
|
OFF_ -> pure OFF
|
|
DEL_ -> pure DEL
|
|
QUE_ -> pure QUE
|
|
CT SRecipientService SUBS_
|
|
| v >= rcvServiceSMPVersion -> Cmd SRecipientService <$> (SUBS <$> _smpP <*> smpP)
|
|
| otherwise -> pure $ Cmd SRecipientService $ SUBS (-1) mempty
|
|
CT SSender tag ->
|
|
Cmd SSender <$> case tag of
|
|
SKEY_ -> SKEY <$> _smpP
|
|
SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP)
|
|
CT SIdleClient PING_ -> pure $ Cmd SIdleClient PING
|
|
CT SProxyService RFWD_ ->
|
|
Cmd SProxyService . RFWD . EncFwdTransmission . unTail <$> _smpP
|
|
CT SSenderLink tag ->
|
|
Cmd SSenderLink <$> case tag of
|
|
LKEY_ -> LKEY <$> _smpP
|
|
LGET_ -> pure LGET
|
|
CT SProxiedClient tag ->
|
|
Cmd SProxiedClient <$> case tag of
|
|
PFWD_ -> PFWD <$> _smpP <*> smpP <*> (EncTransmission . unTail <$> smpP)
|
|
PRXY_ -> PRXY <$> _smpP <*> smpP
|
|
CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB
|
|
CT SNotifierService NSUBS_
|
|
| v >= rcvServiceSMPVersion -> Cmd SNotifierService <$> (NSUBS <$> _smpP <*> smpP)
|
|
| otherwise -> pure $ Cmd SNotifierService $ NSUBS (-1) mempty
|
|
CT SResolver RSLV_ -> Cmd SResolver . RSLV <$> _smpP <* A.takeByteString
|
|
|
|
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials tAuth entId (Cmd p c) = Cmd p <$> checkCredentials tAuth entId c
|
|
{-# INLINE checkCredentials #-}
|
|
|
|
instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
|
type Tag BrokerMsg = BrokerMsgTag
|
|
encodeProtocol v = \case
|
|
IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId, serviceId, serverNtfCreds}
|
|
| v >= newNtfCredsSMPVersion -> ids <> e queueMode <> e linkId <> e serviceId <> e serverNtfCreds
|
|
| v >= serviceCertsSMPVersion -> ids <> e queueMode <> e linkId <> e serviceId
|
|
| v >= shortLinksSMPVersion -> ids <> e queueMode <> e linkId
|
|
| v >= sndAuthKeySMPVersion -> ids <> e (senderCanSecure queueMode)
|
|
| otherwise -> ids
|
|
where
|
|
ids = e (IDS_, ' ', rcvId, sndId, srvDh)
|
|
LNK sId d -> e (LNK_, ' ', sId, d)
|
|
SOK serviceId_
|
|
| v >= serviceCertsSMPVersion -> e (SOK_, ' ', serviceId_)
|
|
| otherwise -> e OK_ -- won't happen, the association with the service requires v >= serviceCertsSMPVersion
|
|
SOKS n idsHash -> serviceResp SOKS_ n idsHash
|
|
MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} ->
|
|
e (MSG_, ' ', msgId, Tail body)
|
|
ALLS -> e ALLS_
|
|
NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh)
|
|
NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta)
|
|
PKEY sid vr certKey -> e (PKEY_, ' ', sid, vr, certKey)
|
|
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
|
|
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
|
|
END -> e END_
|
|
ENDS n idsHash -> serviceResp ENDS_ n idsHash
|
|
DELD
|
|
| v >= deletedEventSMPVersion -> e DELD_
|
|
| otherwise -> e END_
|
|
INFO info -> e (INFO_, ' ', info)
|
|
OK -> e OK_
|
|
ERR err -> e (ERR_, ' ', err')
|
|
where
|
|
err' = case err of
|
|
BLOCKED info
|
|
| v < blockedEntitySMPVersion -> AUTH
|
|
| v < clientNoticesSMPVersion -> BLOCKED info {notice = Nothing}
|
|
_ -> err
|
|
PONG -> e PONG_
|
|
RNAME rec -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode rec)
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
serviceResp tag n idsHash
|
|
| v >= rcvServiceSMPVersion = e (tag, ' ', n, idsHash)
|
|
| otherwise = e (tag, ' ', n)
|
|
|
|
protocolP v = \case
|
|
MSG_ -> do
|
|
msgId <- _smpP
|
|
MSG . RcvMessage msgId <$> bodyP
|
|
where
|
|
bodyP = EncRcvMsgBody . unTail <$> smpP
|
|
ALLS_ -> pure ALLS
|
|
IDS_
|
|
| v >= newNtfCredsSMPVersion -> ids smpP smpP smpP smpP
|
|
| v >= serviceCertsSMPVersion -> ids smpP smpP smpP nothing
|
|
| v >= shortLinksSMPVersion -> ids smpP smpP nothing nothing
|
|
| v >= sndAuthKeySMPVersion -> ids (qm <$> smpP) nothing nothing nothing
|
|
| otherwise -> ids nothing nothing nothing nothing
|
|
where
|
|
qm sndSecure = Just $ if sndSecure then QMMessaging else QMContact
|
|
nothing = pure Nothing
|
|
ids p1 p2 p3 p4 = do
|
|
rcvId <- _smpP
|
|
sndId <- smpP
|
|
rcvPublicDhKey <- smpP
|
|
queueMode <- p1
|
|
linkId <- p2
|
|
serviceId <- p3
|
|
serverNtfCreds <- p4
|
|
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId, serviceId, serverNtfCreds}
|
|
LNK_ -> LNK <$> _smpP <*> smpP
|
|
SOK_ -> SOK <$> _smpP
|
|
SOKS_ -> serviceRespP SOKS
|
|
NID_ -> NID <$> _smpP <*> smpP
|
|
NMSG_ -> NMSG <$> _smpP <*> smpP
|
|
PKEY_ -> PKEY <$> _smpP <*> smpP <*> smpP
|
|
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
|
|
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
|
|
END_ -> pure END
|
|
ENDS_ -> serviceRespP ENDS
|
|
DELD_ -> pure DELD
|
|
INFO_ -> INFO <$> _smpP
|
|
OK_ -> pure OK
|
|
ERR_ -> ERR <$> _smpP
|
|
PONG_ -> pure PONG
|
|
RNAME_ -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP
|
|
where
|
|
serviceRespP resp
|
|
| v >= rcvServiceSMPVersion = resp <$> _smpP <*> smpP
|
|
| otherwise = resp <$> _smpP <*> pure mempty
|
|
|
|
fromProtocolError = \case
|
|
PECmdSyntax -> CMD SYNTAX
|
|
PECmdUnknown -> CMD UNKNOWN
|
|
PESession -> SESSION
|
|
PEBlock -> BLOCK
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials _ (EntityId entId) cmd = case cmd of
|
|
-- IDS response should not have queue ID
|
|
IDS _ -> Right cmd
|
|
-- ERR response does not always have queue ID
|
|
ERR _ -> Right cmd
|
|
-- PONG response must not have queue ID
|
|
PONG -> noEntityMsg
|
|
PKEY {} -> noEntityMsg
|
|
RRES _ -> noEntityMsg
|
|
ALLS -> noEntityMsg
|
|
RNAME _ -> noEntityMsg
|
|
-- other broker responses must have queue ID
|
|
_
|
|
| B.null entId -> Left $ CMD NO_ENTITY
|
|
| otherwise -> Right cmd
|
|
where
|
|
noEntityMsg :: Either ErrorType BrokerMsg
|
|
noEntityMsg
|
|
| B.null entId = Right cmd
|
|
| otherwise = Left $ CMD HAS_AUTH
|
|
|
|
-- | Parse SMP protocol commands and broker messages
|
|
parseProtocol :: forall v err msg. ProtocolEncoding v err msg => Version v -> ByteString -> Either err msg
|
|
parseProtocol v s =
|
|
let (tag, params) = B.break (== ' ') s
|
|
in case decodeTag tag of
|
|
Just cmd -> parse (protocolP v cmd) (fromProtocolError @v @err @msg $ PECmdSyntax) params
|
|
Nothing -> Left $ fromProtocolError @v @err @msg $ PECmdUnknown
|
|
|
|
checkParty :: forall t p p'. (PartyI p, PartyI p') => t p' -> Either String (t p)
|
|
checkParty c = case testEquality (sParty @p) (sParty @p') of
|
|
Just Refl -> Right c
|
|
Nothing -> Left "bad command party"
|
|
|
|
checkParty' :: forall t p p'. (PartyI p, PartyI p') => t p' -> Maybe (t p)
|
|
checkParty' = eitherToMaybe . checkParty
|
|
|
|
instance Encoding ErrorType where
|
|
smpEncode = \case
|
|
BLOCK -> "BLOCK"
|
|
SESSION -> "SESSION"
|
|
CMD err -> "CMD " <> smpEncode err
|
|
PROXY err -> "PROXY " <> smpEncode err
|
|
AUTH -> "AUTH"
|
|
BLOCKED info -> "BLOCKED " <> smpEncode info
|
|
SERVICE -> "SERVICE"
|
|
CRYPTO -> "CRYPTO"
|
|
QUOTA -> "QUOTA"
|
|
STORE err -> "STORE " <> encodeUtf8 err
|
|
EXPIRED -> "EXPIRED"
|
|
NO_MSG -> "NO_MSG"
|
|
LARGE_MSG -> "LARGE_MSG"
|
|
INTERNAL -> "INTERNAL"
|
|
NAME err -> "NAME " <> smpEncode err
|
|
DUPLICATE_ -> "DUPLICATE_"
|
|
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"BLOCK" -> pure BLOCK
|
|
"SESSION" -> pure SESSION
|
|
"CMD" -> CMD <$> _smpP
|
|
"PROXY" -> PROXY <$> _smpP
|
|
"AUTH" -> pure AUTH
|
|
"BLOCKED" -> BLOCKED <$> _smpP
|
|
"SERVICE" -> pure SERVICE
|
|
"CRYPTO" -> pure CRYPTO
|
|
"QUOTA" -> pure QUOTA
|
|
"STORE" -> STORE . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
|
"EXPIRED" -> pure EXPIRED
|
|
"NO_MSG" -> pure NO_MSG
|
|
"LARGE_MSG" -> pure LARGE_MSG
|
|
"INTERNAL" -> pure INTERNAL
|
|
"NAME" -> NAME <$> _smpP
|
|
"DUPLICATE_" -> pure DUPLICATE_
|
|
_ -> fail "bad ErrorType"
|
|
|
|
instance Encoding NameErrorType where
|
|
smpEncode = \case
|
|
NO_RESOLVER -> "NO_RESOLVER"
|
|
NOT_FOUND -> "NOT_FOUND"
|
|
RESOLVER e -> "RESOLVER " <> encodeUtf8 e
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"NO_RESOLVER" -> pure NO_RESOLVER
|
|
"NOT_FOUND" -> pure NOT_FOUND
|
|
"RESOLVER" -> RESOLVER . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
|
_ -> fail "bad NameErrorType"
|
|
|
|
instance StrEncoding NameErrorType where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
instance Encoding CommandError where
|
|
smpEncode e = case e of
|
|
UNKNOWN -> "UNKNOWN"
|
|
SYNTAX -> "SYNTAX"
|
|
PROHIBITED -> "PROHIBITED"
|
|
NO_AUTH -> "NO_AUTH"
|
|
HAS_AUTH -> "HAS_AUTH"
|
|
NO_ENTITY -> "NO_ENTITY"
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"UNKNOWN" -> pure UNKNOWN
|
|
"SYNTAX" -> pure SYNTAX
|
|
"PROHIBITED" -> pure PROHIBITED
|
|
"NO_AUTH" -> pure NO_AUTH
|
|
"HAS_AUTH" -> pure HAS_AUTH
|
|
"NO_ENTITY" -> pure NO_ENTITY
|
|
"NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility
|
|
_ -> fail "bad CommandError"
|
|
|
|
instance Encoding ProxyError where
|
|
smpEncode = \case
|
|
PROTOCOL e -> "PROTOCOL " <> smpEncode e
|
|
BROKER e -> "BROKER " <> smpEncode e
|
|
BASIC_AUTH -> "BASIC_AUTH"
|
|
NO_SESSION -> "NO_SESSION"
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"PROTOCOL" -> PROTOCOL <$> _smpP
|
|
"BROKER" -> BROKER <$> _smpP
|
|
"BASIC_AUTH" -> pure BASIC_AUTH
|
|
"NO_SESSION" -> pure NO_SESSION
|
|
_ -> fail "bad ProxyError"
|
|
|
|
instance StrEncoding ProxyError where
|
|
strEncode = \case
|
|
PROTOCOL e -> "PROTOCOL " <> strEncode e
|
|
BROKER e -> "BROKER " <> strEncode e
|
|
BASIC_AUTH -> "BASIC_AUTH"
|
|
NO_SESSION -> "NO_SESSION"
|
|
strP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"PROTOCOL" -> PROTOCOL <$> _strP
|
|
"BROKER" -> BROKER <$> _strP
|
|
"BASIC_AUTH" -> pure BASIC_AUTH
|
|
"NO_SESSION" -> pure NO_SESSION
|
|
_ -> fail "bad ProxyError"
|
|
|
|
instance Encoding BrokerErrorType where
|
|
smpEncode = \case
|
|
RESPONSE e -> "RESPONSE " <> smpEncode e
|
|
UNEXPECTED e -> "UNEXPECTED " <> smpEncode e
|
|
TRANSPORT e -> "TRANSPORT " <> smpEncode e
|
|
NETWORK _e -> "NETWORK" -- TODO once all upgrade: "NETWORK " <> smpEncode e
|
|
TIMEOUT -> "TIMEOUT"
|
|
HOST -> "HOST"
|
|
NO_SERVICE -> "NO_SERVICE"
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"RESPONSE" -> RESPONSE <$> _smpP
|
|
"UNEXPECTED" -> UNEXPECTED <$> _smpP
|
|
"TRANSPORT" -> TRANSPORT <$> _smpP
|
|
"NETWORK" -> NETWORK <$> (_smpP <|> pure NEFailedError)
|
|
"TIMEOUT" -> pure TIMEOUT
|
|
"HOST" -> pure HOST
|
|
"NO_SERVICE" -> pure NO_SERVICE
|
|
_ -> fail "bad BrokerErrorType"
|
|
|
|
instance StrEncoding BrokerErrorType where
|
|
strEncode = \case
|
|
RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e)
|
|
UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e)
|
|
TRANSPORT e -> "TRANSPORT " <> smpEncode e
|
|
NETWORK _e -> "NETWORK" -- TODO once all upgrade: "NETWORK " <> strEncode e
|
|
TIMEOUT -> "TIMEOUT"
|
|
HOST -> "HOST"
|
|
NO_SERVICE -> "NO_SERVICE"
|
|
strP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"RESPONSE" -> RESPONSE <$> _textP
|
|
"UNEXPECTED" -> UNEXPECTED <$> _textP
|
|
"TRANSPORT" -> TRANSPORT <$> _smpP
|
|
"NETWORK" -> NETWORK <$> (_strP <|> pure NEFailedError)
|
|
"TIMEOUT" -> pure TIMEOUT
|
|
"HOST" -> pure HOST
|
|
"NO_SERVICE" -> pure NO_SERVICE
|
|
_ -> fail "bad BrokerErrorType"
|
|
|
|
instance Encoding NetworkError where
|
|
smpEncode = \case
|
|
NEConnectError e -> "CONNECT " <> smpEncode e
|
|
NETLSError e -> "TLS " <> smpEncode e
|
|
NEUnknownCAError -> "UNKNOWNCA"
|
|
NEFailedError -> "FAILED"
|
|
NETimeoutError -> "TIMEOUT"
|
|
NESubscribeError e -> "SUBSCRIBE " <> smpEncode e
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"CONNECT" -> NEConnectError <$> _smpP
|
|
"TLS" -> NETLSError <$> _smpP
|
|
"UNKNOWNCA" -> pure NEUnknownCAError
|
|
"FAILED" -> pure NEFailedError
|
|
"TIMEOUT" -> pure NETimeoutError
|
|
"SUBSCRIBE" -> NESubscribeError <$> _smpP
|
|
_ -> fail "bad NetworkError"
|
|
|
|
instance StrEncoding NetworkError where
|
|
strEncode = \case
|
|
NEConnectError e -> "CONNECT " <> encodeUtf8 (T.pack e)
|
|
NETLSError e -> "TLS " <> encodeUtf8 (T.pack e)
|
|
NEUnknownCAError -> "UNKNOWNCA"
|
|
NEFailedError -> "FAILED"
|
|
NETimeoutError -> "TIMEOUT"
|
|
NESubscribeError e -> "SUBSCRIBE " <> encodeUtf8 (T.pack e)
|
|
strP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"CONNECT" -> NEConnectError <$> _textP
|
|
"TLS" -> NETLSError <$> _textP
|
|
"UNKNOWNCA" -> pure NEUnknownCAError
|
|
"FAILED" -> pure NEFailedError
|
|
"TIMEOUT" -> pure NETimeoutError
|
|
"SUBSCRIBE" -> NESubscribeError <$> _textP
|
|
_ -> fail "bad NetworkError"
|
|
|
|
_textP :: Parser String
|
|
_textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString)
|
|
|
|
-- | Send signed SMP transmission to TCP transport.
|
|
tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()]
|
|
tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions params
|
|
where
|
|
tPutBatch :: TransportBatch () -> IO [Either TransportError ()]
|
|
tPutBatch = \case
|
|
TBError e _ -> [Left e] <$ putStrLn "tPut error: large message"
|
|
TBTransmissions s n _ -> replicate n <$> tPutLog th s
|
|
TBTransmission s _ -> (: []) <$> tPutLog th s
|
|
|
|
tPutLog :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
|
|
tPutLog th s = do
|
|
r <- tPutBlock th s
|
|
case r of
|
|
Left e -> putStrLn ("tPut error: " <> show e)
|
|
_ -> pure ()
|
|
pure r
|
|
|
|
-- ByteString in TBTransmissions includes byte with transmissions count
|
|
data TransportBatch r = TBTransmissions ByteString Int [r] | TBTransmission ByteString r | TBError TransportError r
|
|
|
|
batchTransmissions :: THandleParams v p -> NonEmpty (Either TransportError SentRawTransmission) -> [TransportBatch ()]
|
|
batchTransmissions params = batchTransmissions' params . L.map (,())
|
|
|
|
-- | encodes and batches transmissions into blocks
|
|
batchTransmissions' :: forall v p r. THandleParams v p -> NonEmpty (Either TransportError SentRawTransmission, r) -> [TransportBatch r]
|
|
batchTransmissions' THandleParams {batch, blockSize = bSize, serviceAuth} ts
|
|
| batch = batchTransmissions_ bSize $ L.map (first $ fmap $ tEncodeForBatch serviceAuth) ts
|
|
| otherwise = map mkBatch1 $ L.toList ts
|
|
where
|
|
mkBatch1 :: (Either TransportError SentRawTransmission, r) -> TransportBatch r
|
|
mkBatch1 (t_, r) = case t_ of
|
|
Left e -> TBError e r
|
|
Right t
|
|
-- 2 bytes are reserved for pad size
|
|
| B.length s <= bSize - 2 -> TBTransmission s r
|
|
| otherwise -> TBError TELargeMsg r
|
|
where
|
|
s = tEncode serviceAuth t
|
|
|
|
-- | Pack encoded transmissions into batches
|
|
batchTransmissions_ :: Int -> NonEmpty (Either TransportError ByteString, r) -> [TransportBatch r]
|
|
batchTransmissions_ bSize = addBatch . foldr addTransmission ([], 0, 0, [], [])
|
|
where
|
|
-- 19 = 2 bytes reserved for pad size + 1 for transmission count + 16 auth tag from block encryption
|
|
bSize' = bSize - 19
|
|
addTransmission :: (Either TransportError ByteString, r) -> ([TransportBatch r], Int, Int, [ByteString], [r]) -> ([TransportBatch r], Int, Int, [ByteString], [r])
|
|
addTransmission (t_, r) acc@(bs, !len, !n, ss, rs) = case t_ of
|
|
Left e -> (TBError e r : addBatch acc, 0, 0, [], [])
|
|
Right s
|
|
| len' <= bSize' && n < 255 -> (bs, len', 1 + n, s : ss, r : rs)
|
|
| sLen <= bSize' -> (addBatch acc, sLen, 1, [s], [r])
|
|
| otherwise -> (TBError TELargeMsg r : addBatch acc, 0, 0, [], [])
|
|
where
|
|
sLen = B.length s
|
|
len' = len + sLen
|
|
addBatch :: ([TransportBatch r], Int, Int, [ByteString], [r]) -> [TransportBatch r]
|
|
addBatch (bs, _len, n, ss, rs) = if n == 0 then bs else TBTransmissions b n rs : bs
|
|
where
|
|
b = B.concat $ B.singleton (lenEncode n) : ss
|
|
|
|
tEncode :: Bool -> SentRawTransmission -> ByteString
|
|
tEncode serviceAuth (auth, t) = tEncodeAuth serviceAuth auth <> t
|
|
{-# INLINE tEncode #-}
|
|
|
|
tEncodeForBatch :: Bool -> SentRawTransmission -> ByteString
|
|
tEncodeForBatch serviceAuth = smpEncode . Large . tEncode serviceAuth
|
|
{-# INLINE tEncodeForBatch #-}
|
|
|
|
tEncodeBatch1 :: Bool -> SentRawTransmission -> ByteString
|
|
tEncodeBatch1 serviceAuth t = lenEncode 1 `B.cons` tEncodeForBatch serviceAuth t
|
|
{-# INLINE tEncodeBatch1 #-}
|
|
|
|
-- tForAuth is lazy to avoid computing it when there is no key to sign
|
|
data TransmissionForAuth = TransmissionForAuth {tForAuth :: ~ByteString, tToSend :: ByteString}
|
|
|
|
encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> TransmissionForAuth
|
|
encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t =
|
|
TransmissionForAuth {tForAuth, tToSend = if implySessId then t' else tForAuth}
|
|
where
|
|
tForAuth = smpEncode sessionId <> t'
|
|
t' = encodeTransmission_ v t
|
|
{-# INLINE encodeTransmissionForAuth #-}
|
|
|
|
encodeTransmission :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> ByteString
|
|
encodeTransmission THandleParams {thVersion = v, sessionId, implySessId} t =
|
|
if implySessId then t' else smpEncode sessionId <> t'
|
|
where
|
|
t' = encodeTransmission_ v t
|
|
{-# INLINE encodeTransmission #-}
|
|
|
|
encodeTransmission_ :: ProtocolEncoding v e c => Version v -> Transmission c -> ByteString
|
|
encodeTransmission_ v (CorrId corrId, queueId, command) =
|
|
smpEncode (corrId, queueId) <> encodeProtocol v command
|
|
{-# INLINE encodeTransmission_ #-}
|
|
|
|
-- | Receive and parse transmission from the TCP transport (ignoring any trailing padding).
|
|
tGetParse :: Transport c => THandle v c p -> IO (NonEmpty (Either TransportError RawTransmission))
|
|
tGetParse th@THandle {params} = eitherList (tParse params) <$> tGetBlock th
|
|
{-# INLINE tGetParse #-}
|
|
|
|
tParse :: THandleParams v p -> ByteString -> NonEmpty (Either TransportError RawTransmission)
|
|
tParse thParams@THandleParams {batch} s
|
|
| batch = eitherList (L.map (\(Large t) -> tParse1 t)) ts
|
|
| otherwise = [tParse1 s]
|
|
where
|
|
tParse1 = parse (transmissionP thParams) TEBadBlock
|
|
ts = parse smpP TEBadBlock s
|
|
|
|
eitherList :: (a -> NonEmpty (Either e b)) -> Either e a -> NonEmpty (Either e b)
|
|
eitherList = either (\e -> [Left e])
|
|
|
|
-- | Receive server transmissions
|
|
tGetServer :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TServer -> IO (NonEmpty (SignedTransmissionOrError err cmd))
|
|
tGetServer = tGet tDecodeServer
|
|
{-# INLINE tGetServer #-}
|
|
|
|
-- | Receive client transmissions
|
|
tGetClient :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TClient -> IO (NonEmpty (Transmission (Either err cmd)))
|
|
tGetClient = tGet tDecodeClient
|
|
{-# INLINE tGetClient #-}
|
|
|
|
tGet ::
|
|
Transport c =>
|
|
(THandleParams v p -> Either TransportError RawTransmission -> r) ->
|
|
THandle v c p ->
|
|
IO (NonEmpty r)
|
|
tGet tDecode th@THandle {params} = L.map (tDecode params) <$> tGetParse th
|
|
{-# INLINE tGet #-}
|
|
|
|
tDecodeServer :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v 'TServer -> Either TransportError RawTransmission -> SignedTransmissionOrError err cmd
|
|
tDecodeServer THandleParams {sessionId, thVersion = v, implySessId} = \case
|
|
Right RawTransmission {authenticator, serviceSig, authorized, sessId, corrId, entityId, command}
|
|
| implySessId || sessId == sessionId -> case decodeTAuthBytes authenticator serviceSig of
|
|
Right tAuth -> bimap t ((tAuth,authorized,) . t) cmdOrErr
|
|
where
|
|
cmdOrErr = parseProtocol @v @err @cmd v command >>= checkCredentials tAuth entityId
|
|
t :: a -> (CorrId, EntityId, a)
|
|
t = (corrId,entityId,)
|
|
Left _ -> tError corrId PEBlock
|
|
| otherwise -> tError corrId PESession
|
|
Left _ -> tError "" PEBlock
|
|
where
|
|
tError :: CorrId -> ProtocolErrorType -> SignedTransmissionOrError err cmd
|
|
tError corrId err = Left (corrId, NoEntity, fromProtocolError @v @err @cmd err)
|
|
|
|
tDecodeClient :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v 'TClient -> Either TransportError RawTransmission -> Transmission (Either err cmd)
|
|
tDecodeClient THandleParams {sessionId, thVersion = v, implySessId} = \case
|
|
Right RawTransmission {sessId, corrId, entityId, command}
|
|
| implySessId || sessId == sessionId -> (corrId, entityId, cmdOrErr)
|
|
| otherwise -> tError corrId PESession
|
|
where
|
|
cmdOrErr = parseProtocol @v @err @cmd v command >>= checkCredentials Nothing entityId
|
|
Left _ -> tError "" PEBlock
|
|
where
|
|
tError :: CorrId -> ProtocolErrorType -> Transmission (Either err cmd)
|
|
tError corrId err = (corrId, NoEntity, Left $ fromProtocolError @v @err @cmd err)
|
|
|
|
$(J.deriveJSON defaultJSON ''MsgFlags)
|
|
|
|
$(J.deriveJSON (sumTypeJSON id) ''CommandError)
|
|
|
|
$(J.deriveToJSON (sumTypeJSON $ dropPrefix "NE") ''NetworkError)
|
|
|
|
instance FromJSON NetworkError where
|
|
parseJSON = $(J.mkParseJSON (sumTypeJSON $ dropPrefix "NE") ''NetworkError)
|
|
omittedField = Just NEFailedError
|
|
|
|
$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType)
|
|
|
|
$(J.deriveJSON defaultJSON ''BlockingInfo)
|
|
|
|
-- run deriveJSON in one TH splice to allow mutual instance
|
|
$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''NameErrorType, ''ErrorType])
|