mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-07-10 12:13:07 +00:00
f0b7a4be73
* smp server: messaging services (#1565)
* smp server: refactor message delivery to always respond SOK to subscriptions
* refactor ntf subscribe
* cancel subscription thread and reduce service subscription count when queue is deleted
* subscribe rcv service, deliver sent messages to subscribed service
* subscribe rcv service to messages (TODO delivery on subscription)
* WIP
* efficient initial delivery of messages to subscribed service
* test: delivery to client with service certificate
* test: upgrade/downgrade to/from service subscriptions
* remove service association from agent API, add per-user flag to use the service
* agent client (WIP)
* service certificates in the client
* rfc about drift detection, and SALL to mark end of message delivery
* fix test
* fix test
* add function for postgresql message storage
* update migration
* servers: maintain xor-hash of all associated queue IDs in PostgreSQL (#1668)
* servers: maintain xor-hash of all associated queue IDs in PostgreSQL (#1615)
* ntf server: maintain xor-hash of all associated queue IDs via PostgreSQL triggers
* smp server: xor hash with triggers
* fix sql and using pgcrypto extension in tests
* track counts and hashes in smp/ntf servers via triggers, smp server stats for service subscription, update SMP protocol to pass expected count and hash in SSUB/NSSUB commands
* agent migrations with functions/triggers
* remove agent triggers
* try tracking service subs in the agent (WIP, does not compile)
* Revert "try tracking service subs in the agent (WIP, does not compile)"
This reverts commit 59e908100d.
* comment
* agent database triggers
* service subscriptions in the client
* test / fix client services
* update schema
* fix postgres migration
* update schema
* move schema test to the end
* use static function with SQLite to avoid dynamic wrapper
* agent: fail when per-connection transport isolation is used with services (#1670)
* agent: service subscription events (#1671)
* agent: use server keyhash when loading service record
* agent: process queue/service associations with delayed subscription results
* agent: service subscription events
* agent: finalize initial service subscriptions, remove associations on service ID changes (#1672)
* agent: remove service/queue associations when service ID changes
* agent: check that service ID in NEW response matches session ID in transport session
* agent subscription WIP
* test
* comment
* enable tests
* update queries
* agent: option to add SQLite aggregates to DB connection (#1673)
* agent: add build_relations_vector function to sqlite
* update aggregate
* use static aggregate
* remove relations
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* add test, treat BAD_SERVICE as temp error, only remove queue associations on service errors
* add packZipWith for backward compatibility with GHC 8.10.7
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
* servers: service stats and logging, allow services without option (removed), report errors during service message delivery, remove threads when service subscription ended (#1676)
* smp server: always allow services without option
* smp server: maintain IDs hash in session subscription states
* smp server: service message delivery error handling
* ntf server: log subscription count and hash differences
* smp server: remove delivery threads when service subscription ended/client disconnected
* agent: remove service queue association when service ID changed, process ENDS event, test migrating to/from service (#1677)
* agent: remove service queue association when service ID changed
* agent: process ENDS event
* agent: send service subscription error event
* agent: test migrating to/from service subscriptions, fixes
* agent: always remove service when disabled, fix service subscriptions
* ntf server: use different client certs for each SMP server, remove support for store log (#1681)
* ntf server: remove support for store log
* ntf server: use different client certificates for each SMP server
* smp protocol: fix encoding for SOKS/ENDS responses (#1683)
* agent: create user with option to enable client service (#1684)
* agent: create user with option to enable client service
* handle HTTP2 errors
* do not catch async exceptions
* agent: minor fixes
* docs: update protocol (#1705)
* docs: agent threat model
* update protocol docs
* update RFCs (#1730)
* update RFCs
* update
* update overview
* update terminology
* original language in threat model
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* docs: fix minor issues in protocols
* docs: add e2e encrypted message wire encoding to PQDR spec
* docs: add missing encodings and other protocol corrections
* docs: move implemented rfcs
* smp: service fixes (#1737)
* smp: deliver service subscription to correct client
* tests: more resilient to concurrency
* optimize PostgreSQL query
* fix service re-association after server "downgrade"
* correctly handle service removed from server (and ID changed)
* remove unused
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* prometheus: fix metrics names (#1747)
* test: rcv service re-association on restart (#1746)
* agent: correct log message
* docs: update whitepaper
* smp: fix messaging client service issues (#1751)
* services: fix minor issues
* fix accounting for subscribed service queues, add prometheus stats
* fix uncorrelated subquery
* fix potential race condition when inserting service defensively, as it is also prevented by how client is created
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* agent: refactor cleanup if no pending subs (#1757)
* smp server: batch processing of subscription messages (#1753)
* smp server: batch processing of subscription messages
* refactor
* empty line
* fix
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* smp: batch queue association updates on subscriptions (#1760)
* smp: batch queue association updates on subscriptions
* refactor to fused batching
* simpler
* batch assoc functions
* clean up
* fix
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* agent: use primary key index in setRcvServiceAssocs (#1783)
* agent: use primary key index in setRcvServiceAssocs
Previous WHERE rcv_id = ? did not match the (host, port, rcv_id)
primary key prefix and fell back to a table scan via
idx_rcv_queues_client_notice_id. With ~390k rows per queue, each
update in a 1350-row batch scanned the whole table, yielding ~290s
per batch and a multi-hour rcv-services migration.
* agent: pass SMPServer explicitly to setRcvServiceAssocs
Avoid extracting host/port from the first queue inside setRcvServiceAssocs.
The caller already has SMPServer in scope (from tSess) and the call chain
is short, so threading it through is simpler than inspecting the list.
Removes the empty-list guard from setRcvServiceAssocs (it remains in
processRcvServiceAssocs).
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
653 lines
20 KiB
Haskell
653 lines
20 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
module Simplex.Messaging.Notifications.Protocol
|
|
( NtfEntity (..),
|
|
SNtfEntity (..),
|
|
NtfEntityI (..),
|
|
NtfCommandTag (..),
|
|
NtfCmdTag (..),
|
|
NtfRegCode (..),
|
|
NewNtfEntity (..),
|
|
ANewNtfEntity (..),
|
|
NtfCommand (..),
|
|
NtfCmd (..),
|
|
NtfResponseTag (..),
|
|
NtfResponse (..),
|
|
SMPQueueNtf (..),
|
|
PushProvider (..),
|
|
DeviceToken (..),
|
|
PNMessageData (..),
|
|
NtfEntityId,
|
|
NtfSubscriptionId,
|
|
NtfTokenId,
|
|
NtfSubStatus (..),
|
|
NtfTknStatus (..),
|
|
NTInvalidReason (..),
|
|
encodePNMessages,
|
|
pnMessagesP,
|
|
subscribeNtfStatuses,
|
|
allowTokenVerification,
|
|
allowNtfSubCommands,
|
|
checkEntity,
|
|
) where
|
|
|
|
import Control.Applicative (optional, (<|>))
|
|
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.Encoding as JE
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.Functor (($>))
|
|
import Data.Kind
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import qualified Data.List.NonEmpty as L
|
|
import Data.Maybe (isNothing)
|
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
|
import Data.Time.Clock.System
|
|
import Data.Type.Equality
|
|
import Data.Word (Word16)
|
|
import Simplex.Messaging.Agent.Protocol (updateSMPServerHosts)
|
|
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Notifications.Transport (NTFVersion, invalidReasonNTFVersion, ntfClientHandshake)
|
|
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..))
|
|
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
|
|
|
data NtfEntity = Token | Subscription
|
|
deriving (Show)
|
|
|
|
data SNtfEntity :: NtfEntity -> Type where
|
|
SToken :: SNtfEntity 'Token
|
|
SSubscription :: SNtfEntity 'Subscription
|
|
|
|
instance TestEquality SNtfEntity where
|
|
testEquality SToken SToken = Just Refl
|
|
testEquality SSubscription SSubscription = Just Refl
|
|
testEquality _ _ = Nothing
|
|
|
|
deriving instance Show (SNtfEntity e)
|
|
|
|
class NtfEntityI (e :: NtfEntity) where sNtfEntity :: SNtfEntity e
|
|
|
|
instance NtfEntityI 'Token where sNtfEntity = SToken
|
|
|
|
instance NtfEntityI 'Subscription where sNtfEntity = SSubscription
|
|
|
|
data NtfCommandTag (e :: NtfEntity) where
|
|
TNEW_ :: NtfCommandTag 'Token
|
|
TVFY_ :: NtfCommandTag 'Token
|
|
TCHK_ :: NtfCommandTag 'Token
|
|
TRPL_ :: NtfCommandTag 'Token
|
|
TDEL_ :: NtfCommandTag 'Token
|
|
TCRN_ :: NtfCommandTag 'Token
|
|
SNEW_ :: NtfCommandTag 'Subscription
|
|
SCHK_ :: NtfCommandTag 'Subscription
|
|
SDEL_ :: NtfCommandTag 'Subscription
|
|
PING_ :: NtfCommandTag 'Subscription
|
|
|
|
deriving instance Show (NtfCommandTag e)
|
|
|
|
data NtfCmdTag = forall e. NtfEntityI e => NCT (SNtfEntity e) (NtfCommandTag e)
|
|
|
|
instance NtfEntityI e => Encoding (NtfCommandTag e) where
|
|
smpEncode = \case
|
|
TNEW_ -> "TNEW"
|
|
TVFY_ -> "TVFY"
|
|
TCHK_ -> "TCHK"
|
|
TRPL_ -> "TRPL"
|
|
TDEL_ -> "TDEL"
|
|
TCRN_ -> "TCRN"
|
|
SNEW_ -> "SNEW"
|
|
SCHK_ -> "SCHK"
|
|
SDEL_ -> "SDEL"
|
|
PING_ -> "PING"
|
|
smpP = messageTagP
|
|
|
|
instance Encoding NtfCmdTag where
|
|
smpEncode (NCT _ t) = smpEncode t
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag NtfCmdTag where
|
|
decodeTag = \case
|
|
"TNEW" -> Just $ NCT SToken TNEW_
|
|
"TVFY" -> Just $ NCT SToken TVFY_
|
|
"TCHK" -> Just $ NCT SToken TCHK_
|
|
"TRPL" -> Just $ NCT SToken TRPL_
|
|
"TDEL" -> Just $ NCT SToken TDEL_
|
|
"TCRN" -> Just $ NCT SToken TCRN_
|
|
"SNEW" -> Just $ NCT SSubscription SNEW_
|
|
"SCHK" -> Just $ NCT SSubscription SCHK_
|
|
"SDEL" -> Just $ NCT SSubscription SDEL_
|
|
"PING" -> Just $ NCT SSubscription PING_
|
|
_ -> Nothing
|
|
|
|
instance NtfEntityI e => ProtocolMsgTag (NtfCommandTag e) where
|
|
decodeTag s = decodeTag s >>= (\(NCT _ t) -> checkEntity' t)
|
|
|
|
newtype NtfRegCode = NtfRegCode ByteString
|
|
deriving (Eq, Show)
|
|
|
|
instance Encoding NtfRegCode where
|
|
smpEncode (NtfRegCode code) = smpEncode code
|
|
smpP = NtfRegCode <$> smpP
|
|
|
|
instance StrEncoding NtfRegCode where
|
|
strEncode (NtfRegCode m) = strEncode m
|
|
strDecode s = NtfRegCode <$> strDecode s
|
|
strP = NtfRegCode <$> strP
|
|
|
|
instance FromJSON NtfRegCode where
|
|
parseJSON = strParseJSON "NtfRegCode"
|
|
|
|
instance ToJSON NtfRegCode where
|
|
toJSON = strToJSON
|
|
toEncoding = strToJEncoding
|
|
|
|
data NewNtfEntity (e :: NtfEntity) where
|
|
NewNtfTkn :: DeviceToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> NewNtfEntity 'Token
|
|
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NtfPrivateAuthKey -> NewNtfEntity 'Subscription
|
|
|
|
deriving instance Show (NewNtfEntity e)
|
|
|
|
data ANewNtfEntity = forall e. NtfEntityI e => ANE (SNtfEntity e) (NewNtfEntity e)
|
|
|
|
deriving instance Show ANewNtfEntity
|
|
|
|
instance NtfEntityI e => Encoding (NewNtfEntity e) where
|
|
smpEncode = \case
|
|
NewNtfTkn tkn verifyKey dhPubKey -> smpEncode ('T', tkn, verifyKey, dhPubKey)
|
|
NewNtfSub tknId smpQueue notifierKey -> smpEncode ('S', tknId, smpQueue, notifierKey)
|
|
smpP = (\(ANE _ c) -> checkEntity c) <$?> smpP
|
|
|
|
instance Encoding ANewNtfEntity where
|
|
smpEncode (ANE _ e) = smpEncode e
|
|
smpP =
|
|
A.anyChar >>= \case
|
|
'T' -> ANE SToken <$> (NewNtfTkn <$> smpP <*> smpP <*> smpP)
|
|
'S' -> ANE SSubscription <$> (NewNtfSub <$> smpP <*> smpP <*> smpP)
|
|
_ -> fail "bad ANewNtfEntity"
|
|
|
|
instance Protocol NTFVersion ErrorType NtfResponse where
|
|
type ProtoCommand NtfResponse = NtfCmd
|
|
type ProtoType NtfResponse = 'PNTF
|
|
protocolClientHandshake c _ks = ntfClientHandshake c
|
|
{-# INLINE protocolClientHandshake #-}
|
|
useServiceAuth _ = False
|
|
{-# INLINE useServiceAuth #-}
|
|
protocolPing = NtfCmd SSubscription PING
|
|
{-# INLINE protocolPing #-}
|
|
protocolError = \case
|
|
NRErr e -> Just e
|
|
_ -> Nothing
|
|
{-# INLINE protocolError #-}
|
|
|
|
data NtfCommand (e :: NtfEntity) where
|
|
-- | register new device token for notifications
|
|
TNEW :: NewNtfEntity 'Token -> NtfCommand 'Token
|
|
-- | verify token - uses e2e encrypted random string sent to the device via PN to confirm that the device has the token
|
|
TVFY :: NtfRegCode -> NtfCommand 'Token
|
|
-- | check token status
|
|
TCHK :: NtfCommand 'Token
|
|
-- | replace device token (while keeping all existing subscriptions)
|
|
TRPL :: DeviceToken -> NtfCommand 'Token
|
|
-- | delete token - all subscriptions will be removed and no more notifications will be sent
|
|
TDEL :: NtfCommand 'Token
|
|
-- | enable periodic background notification to fetch the new messages - interval is in minutes, minimum is 20, 0 to disable
|
|
TCRN :: Word16 -> NtfCommand 'Token
|
|
-- | create SMP subscription
|
|
SNEW :: NewNtfEntity 'Subscription -> NtfCommand 'Subscription
|
|
-- | check SMP subscription status (response is SUB)
|
|
SCHK :: NtfCommand 'Subscription
|
|
-- | delete SMP subscription
|
|
SDEL :: NtfCommand 'Subscription
|
|
-- | keep-alive command
|
|
PING :: NtfCommand 'Subscription
|
|
|
|
deriving instance Show (NtfCommand e)
|
|
|
|
data NtfCmd = forall e. NtfEntityI e => NtfCmd (SNtfEntity e) (NtfCommand e)
|
|
|
|
deriving instance Show NtfCmd
|
|
|
|
instance NtfEntityI e => ProtocolEncoding NTFVersion ErrorType (NtfCommand e) where
|
|
type Tag (NtfCommand e) = NtfCommandTag e
|
|
encodeProtocol _v = \case
|
|
TNEW newTkn -> e (TNEW_, ' ', newTkn)
|
|
TVFY code -> e (TVFY_, ' ', code)
|
|
TCHK -> e TCHK_
|
|
TRPL tkn -> e (TRPL_, ' ', tkn)
|
|
TDEL -> e TDEL_
|
|
TCRN int -> e (TCRN_, ' ', int)
|
|
SNEW newSub -> e (SNEW_, ' ', newSub)
|
|
SCHK -> e SCHK_
|
|
SDEL -> e SDEL_
|
|
PING -> e PING_
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
|
|
protocolP _v tag = (\(NtfCmd _ c) -> checkEntity c) <$?> protocolP _v (NCT (sNtfEntity @e) tag)
|
|
|
|
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials auth (EntityId entityId) cmd = case cmd of
|
|
-- TNEW and SNEW must have signature but NOT token/subscription IDs
|
|
TNEW {} -> sigNoEntity
|
|
SNEW {} -> sigNoEntity
|
|
PING
|
|
| isNothing auth && B.null entityId -> Right cmd
|
|
| otherwise -> Left $ CMD HAS_AUTH
|
|
-- other client commands must have both signature and entity ID
|
|
_
|
|
| isNothing auth || B.null entityId -> Left $ CMD NO_AUTH
|
|
| otherwise -> Right cmd
|
|
where
|
|
sigNoEntity
|
|
| isNothing auth = Left $ CMD NO_AUTH
|
|
| not (B.null entityId) = Left $ CMD HAS_AUTH
|
|
| otherwise = Right cmd
|
|
|
|
instance ProtocolEncoding NTFVersion ErrorType NtfCmd where
|
|
type Tag NtfCmd = NtfCmdTag
|
|
encodeProtocol _v (NtfCmd _ c) = encodeProtocol _v c
|
|
|
|
protocolP _v = \case
|
|
NCT SToken tag ->
|
|
NtfCmd SToken <$> case tag of
|
|
TNEW_ -> TNEW <$> _smpP
|
|
TVFY_ -> TVFY <$> _smpP
|
|
TCHK_ -> pure TCHK
|
|
TRPL_ -> TRPL <$> _smpP
|
|
TDEL_ -> pure TDEL
|
|
TCRN_ -> TCRN <$> _smpP
|
|
NCT SSubscription tag ->
|
|
NtfCmd SSubscription <$> case tag of
|
|
SNEW_ -> SNEW <$> _smpP
|
|
SCHK_ -> pure SCHK
|
|
SDEL_ -> pure SDEL
|
|
PING_ -> pure PING
|
|
|
|
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials tAuth entId (NtfCmd e c) = NtfCmd e <$> checkCredentials tAuth entId c
|
|
|
|
data NtfResponseTag
|
|
= NRTknId_
|
|
| NRSubId_
|
|
| NROk_
|
|
| NRErr_
|
|
| NRTkn_
|
|
| NRSub_
|
|
| NRPong_
|
|
deriving (Show)
|
|
|
|
instance Encoding NtfResponseTag where
|
|
smpEncode = \case
|
|
NRTknId_ -> "IDTKN" -- it should be "TID", "SID"
|
|
NRSubId_ -> "IDSUB"
|
|
NROk_ -> "OK"
|
|
NRErr_ -> "ERR"
|
|
NRTkn_ -> "TKN"
|
|
NRSub_ -> "SUB"
|
|
NRPong_ -> "PONG"
|
|
smpP = messageTagP
|
|
|
|
instance ProtocolMsgTag NtfResponseTag where
|
|
decodeTag = \case
|
|
"IDTKN" -> Just NRTknId_
|
|
"IDSUB" -> Just NRSubId_
|
|
"OK" -> Just NROk_
|
|
"ERR" -> Just NRErr_
|
|
"TKN" -> Just NRTkn_
|
|
"SUB" -> Just NRSub_
|
|
"PONG" -> Just NRPong_
|
|
_ -> Nothing
|
|
|
|
data NtfResponse
|
|
= NRTknId NtfEntityId C.PublicKeyX25519
|
|
| NRSubId NtfEntityId
|
|
| NROk
|
|
| NRErr ErrorType
|
|
| NRTkn NtfTknStatus
|
|
| NRSub NtfSubStatus
|
|
| NRPong
|
|
deriving (Show)
|
|
|
|
instance ProtocolEncoding NTFVersion ErrorType NtfResponse where
|
|
type Tag NtfResponse = NtfResponseTag
|
|
encodeProtocol v = \case
|
|
NRTknId entId dhKey -> e (NRTknId_, ' ', entId, dhKey)
|
|
NRSubId entId -> e (NRSubId_, ' ', entId)
|
|
NROk -> e NROk_
|
|
NRErr err -> e (NRErr_, ' ', err)
|
|
NRTkn stat -> e (NRTkn_, ' ', stat')
|
|
where
|
|
stat'
|
|
| v >= invalidReasonNTFVersion = stat
|
|
| otherwise = case stat of
|
|
NTInvalid _ -> NTInvalid Nothing
|
|
_ -> stat
|
|
NRSub stat -> e (NRSub_, ' ', stat)
|
|
NRPong -> e NRPong_
|
|
where
|
|
e :: Encoding a => a -> ByteString
|
|
e = smpEncode
|
|
|
|
protocolP _v = \case
|
|
NRTknId_ -> NRTknId <$> _smpP <*> smpP
|
|
NRSubId_ -> NRSubId <$> _smpP
|
|
NROk_ -> pure NROk
|
|
NRErr_ -> NRErr <$> _smpP
|
|
NRTkn_ -> NRTkn <$> _smpP
|
|
NRSub_ -> NRSub <$> _smpP
|
|
NRPong_ -> pure NRPong
|
|
|
|
fromProtocolError = \case
|
|
PECmdSyntax -> CMD SYNTAX
|
|
PECmdUnknown -> CMD UNKNOWN
|
|
PESession -> SESSION
|
|
PEBlock -> BLOCK
|
|
{-# INLINE fromProtocolError #-}
|
|
|
|
checkCredentials _ (EntityId entId) cmd = case cmd of
|
|
-- IDTKN response must not have queue ID
|
|
NRTknId {} -> noEntity
|
|
-- IDSUB response must not have queue ID
|
|
NRSubId {} -> noEntity
|
|
-- ERR response does not always have entity ID
|
|
NRErr _ -> Right cmd
|
|
-- PONG response must not have queue ID
|
|
NRPong -> noEntity
|
|
-- other server responses must have entity ID
|
|
_
|
|
| B.null entId -> Left $ CMD NO_ENTITY
|
|
| otherwise -> Right cmd
|
|
where
|
|
noEntity
|
|
| B.null entId = Right cmd
|
|
| otherwise = Left $ CMD HAS_AUTH
|
|
|
|
data SMPQueueNtf = SMPQueueNtf
|
|
{ smpServer :: SMPServer,
|
|
notifierId :: NotifierId
|
|
}
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding SMPQueueNtf where
|
|
smpEncode SMPQueueNtf {smpServer, notifierId} = smpEncode (smpServer, notifierId)
|
|
smpP = do
|
|
smpServer <- updateSMPServerHosts <$> smpP
|
|
notifierId <- smpP
|
|
pure SMPQueueNtf {smpServer, notifierId}
|
|
|
|
instance StrEncoding SMPQueueNtf where
|
|
strEncode SMPQueueNtf {smpServer, notifierId} = strEncode smpServer <> "/" <> strEncode notifierId
|
|
strP = do
|
|
smpServer <- updateSMPServerHosts <$> strP
|
|
notifierId <- A.char '/' *> strP
|
|
pure SMPQueueNtf {smpServer, notifierId}
|
|
|
|
data PushProvider
|
|
= PPApnsDev -- provider for Apple development environment
|
|
| PPApnsProd -- production environment, including TestFlight
|
|
| PPApnsTest -- used for tests, to use APNS mock server
|
|
| PPApnsNull -- used to test servers from the client - does not communicate with APNS
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding PushProvider where
|
|
smpEncode = \case
|
|
PPApnsDev -> "AD"
|
|
PPApnsProd -> "AP"
|
|
PPApnsTest -> "AT"
|
|
PPApnsNull -> "AN"
|
|
smpP =
|
|
A.take 2 >>= \case
|
|
"AD" -> pure PPApnsDev
|
|
"AP" -> pure PPApnsProd
|
|
"AT" -> pure PPApnsTest
|
|
"AN" -> pure PPApnsNull
|
|
_ -> fail "bad PushProvider"
|
|
|
|
instance StrEncoding PushProvider where
|
|
strEncode = \case
|
|
PPApnsDev -> "apns_dev"
|
|
PPApnsProd -> "apns_prod"
|
|
PPApnsTest -> "apns_test"
|
|
PPApnsNull -> "apns_null"
|
|
strP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"apns_dev" -> pure PPApnsDev
|
|
"apns_prod" -> pure PPApnsProd
|
|
"apns_test" -> pure PPApnsTest
|
|
"apns_null" -> pure PPApnsNull
|
|
_ -> fail "bad PushProvider"
|
|
|
|
instance FromField PushProvider where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
|
|
|
|
instance ToField PushProvider where toField = toField . decodeLatin1 . strEncode
|
|
|
|
data DeviceToken = DeviceToken PushProvider ByteString
|
|
deriving (Eq, Ord, Show)
|
|
|
|
instance Encoding DeviceToken where
|
|
smpEncode (DeviceToken p t) = smpEncode (p, t)
|
|
smpP = DeviceToken <$> smpP <*> smpP
|
|
|
|
instance StrEncoding DeviceToken where
|
|
strEncode (DeviceToken p t) = strEncode p <> " " <> t
|
|
strP = nullToken <|> hexToken
|
|
where
|
|
nullToken = "apns_null test_ntf_token" $> DeviceToken PPApnsNull "test_ntf_token"
|
|
hexToken = DeviceToken <$> strP <* A.space <*> hexStringP
|
|
hexStringP =
|
|
A.takeWhile (`B.elem` "0123456789abcdef") >>= \s ->
|
|
if even (B.length s) then pure s else fail "odd number of hex characters"
|
|
|
|
instance ToJSON DeviceToken where
|
|
toEncoding (DeviceToken pp t) = J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t
|
|
toJSON (DeviceToken pp t) = J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t]
|
|
|
|
instance FromJSON DeviceToken where
|
|
parseJSON = J.withObject "DeviceToken" $ \o -> do
|
|
pp <- strDecode . encodeUtf8 <$?> o .: "pushProvider"
|
|
t <- encodeUtf8 <$> o .: "token"
|
|
pure $ DeviceToken pp t
|
|
|
|
-- List of PNMessageData uses semicolon-separated encoding instead of strEncode,
|
|
-- because strEncode of NonEmpty list uses comma for separator,
|
|
-- and encoding of PNMessageData's smpQueue has comma in list of hosts
|
|
encodePNMessages :: NonEmpty PNMessageData -> ByteString
|
|
encodePNMessages = B.intercalate ";" . map strEncode . L.toList
|
|
|
|
pnMessagesP :: A.Parser (NonEmpty PNMessageData)
|
|
pnMessagesP = L.fromList <$> strP `A.sepBy1` A.char ';'
|
|
|
|
data PNMessageData = PNMessageData
|
|
{ smpQueue :: SMPQueueNtf,
|
|
ntfTs :: SystemTime,
|
|
nmsgNonce :: C.CbNonce,
|
|
encNMsgMeta :: EncNMsgMeta
|
|
}
|
|
deriving (Show)
|
|
|
|
instance StrEncoding PNMessageData where
|
|
strEncode PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta} =
|
|
strEncode (smpQueue, ntfTs, nmsgNonce, encNMsgMeta)
|
|
strP = do
|
|
(smpQueue, ntfTs, nmsgNonce, encNMsgMeta) <- strP
|
|
pure PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}
|
|
|
|
type NtfEntityId = EntityId
|
|
|
|
type NtfSubscriptionId = NtfEntityId
|
|
|
|
type NtfTokenId = NtfEntityId
|
|
|
|
data NtfSubStatus
|
|
= -- | state after SNEW
|
|
NSNew
|
|
| -- | pending connection/subscription to SMP server
|
|
NSPending
|
|
| -- | connected and subscribed to SMP server
|
|
NSActive
|
|
| -- | disconnected/unsubscribed from SMP server
|
|
NSInactive
|
|
| -- | END received
|
|
NSEnd
|
|
| -- | DELD received (connection was deleted)
|
|
NSDeleted
|
|
| -- | SMP AUTH error
|
|
NSAuth
|
|
| -- | SMP SERVICE error - rejected service signature on individual subscriptions
|
|
NSService
|
|
| -- | SMP error other than AUTH
|
|
NSErr ByteString
|
|
deriving (Eq, Ord, Show)
|
|
|
|
-- if these statuses change, the queue ID hashes for services need to be updated in a new migration (see m20250830_queue_ids_hash)
|
|
subscribeNtfStatuses :: [NtfSubStatus]
|
|
subscribeNtfStatuses = [NSNew, NSPending, NSActive, NSInactive]
|
|
|
|
instance Encoding NtfSubStatus where
|
|
smpEncode = \case
|
|
NSNew -> "NEW"
|
|
NSPending -> "PENDING" -- e.g. after SMP server disconnect/timeout while ntf server is retrying to connect
|
|
NSActive -> "ACTIVE"
|
|
NSInactive -> "INACTIVE"
|
|
NSEnd -> "END"
|
|
NSDeleted -> "DELETED"
|
|
NSAuth -> "AUTH"
|
|
NSService -> "SERVICE"
|
|
NSErr err -> "ERR " <> err
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"NEW" -> pure NSNew
|
|
"PENDING" -> pure NSPending
|
|
"ACTIVE" -> pure NSActive
|
|
"INACTIVE" -> pure NSInactive
|
|
"END" -> pure NSEnd
|
|
"DELETED" -> pure NSDeleted
|
|
"AUTH" -> pure NSAuth
|
|
"SERVICE" -> pure NSService
|
|
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
|
|
_ -> fail "bad NtfSubStatus"
|
|
|
|
instance StrEncoding NtfSubStatus where
|
|
strEncode = smpEncode
|
|
{-# INLINE strEncode #-}
|
|
strP = smpP
|
|
{-# INLINE strP #-}
|
|
|
|
data NtfTknStatus
|
|
= -- | Token created in DB
|
|
NTNew
|
|
| -- | state after registration (TNEW)
|
|
NTRegistered
|
|
| -- | if initial notification failed (push provider error) or verification failed
|
|
NTInvalid (Maybe NTInvalidReason)
|
|
| -- | Token confirmed via notification (accepted by push provider or verification code received by client)
|
|
NTConfirmed
|
|
| -- | after successful verification (TVFY)
|
|
NTActive
|
|
| -- | after it is no longer valid (push provider error)
|
|
NTExpired
|
|
deriving (Eq, Show)
|
|
|
|
allowTokenVerification :: NtfTknStatus -> Bool
|
|
allowTokenVerification = \case
|
|
NTNew -> False
|
|
NTRegistered -> True
|
|
NTInvalid _ -> False
|
|
NTConfirmed -> True
|
|
NTActive -> True
|
|
NTExpired -> False
|
|
|
|
allowNtfSubCommands :: NtfTknStatus -> Bool
|
|
allowNtfSubCommands = \case
|
|
NTNew -> False
|
|
NTRegistered -> False
|
|
-- TODO we could have separate statuses to show whether it became invalid
|
|
-- after verification (allow commands) or before (do not allow)
|
|
NTInvalid _ -> True
|
|
NTConfirmed -> False
|
|
NTActive -> True
|
|
NTExpired -> True
|
|
|
|
instance Encoding NtfTknStatus where
|
|
smpEncode = \case
|
|
NTNew -> "NEW"
|
|
NTRegistered -> "REGISTERED"
|
|
NTInvalid r_ -> "INVALID" <> maybe "" (\r -> ',' `B.cons` strEncode r) r_
|
|
NTConfirmed -> "CONFIRMED"
|
|
NTActive -> "ACTIVE"
|
|
NTExpired -> "EXPIRED"
|
|
smpP =
|
|
A.takeTill (\c -> c == ' ' || c == ',') >>= \case
|
|
"NEW" -> pure NTNew
|
|
"REGISTERED" -> pure NTRegistered
|
|
"INVALID" -> NTInvalid <$> optional (A.char ',' *> strP)
|
|
"CONFIRMED" -> pure NTConfirmed
|
|
"ACTIVE" -> pure NTActive
|
|
"EXPIRED" -> pure NTExpired
|
|
_ -> fail "bad NtfTknStatus"
|
|
|
|
instance StrEncoding NTInvalidReason where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
data NTInvalidReason = NTIRBadToken | NTIRTokenNotForTopic | NTIRExpiredToken | NTIRUnregistered
|
|
deriving (Eq, Show)
|
|
|
|
instance Encoding NTInvalidReason where
|
|
smpEncode = \case
|
|
NTIRBadToken -> "BAD"
|
|
NTIRTokenNotForTopic -> "TOPIC"
|
|
NTIRExpiredToken -> "EXPIRED"
|
|
NTIRUnregistered -> "UNREGISTERED"
|
|
smpP =
|
|
A.takeTill (== ' ') >>= \case
|
|
"BAD" -> pure NTIRBadToken
|
|
"TOPIC" -> pure NTIRTokenNotForTopic
|
|
"EXPIRED" -> pure NTIRExpiredToken
|
|
"UNREGISTERED" -> pure NTIRUnregistered
|
|
_ -> fail "bad NTInvalidReason"
|
|
|
|
instance StrEncoding NtfTknStatus where
|
|
strEncode = smpEncode
|
|
strP = smpP
|
|
|
|
instance FromField NtfTknStatus where fromField = fromTextField_ $ either (const Nothing) Just . smpDecode . encodeUtf8
|
|
|
|
instance ToField NtfTknStatus where toField = toField . decodeLatin1 . smpEncode
|
|
|
|
instance ToJSON NtfTknStatus where
|
|
toEncoding = JE.text . decodeLatin1 . smpEncode
|
|
toJSON = J.String . decodeLatin1 . smpEncode
|
|
|
|
instance FromJSON NtfTknStatus where
|
|
parseJSON = J.withText "NtfTknStatus" $ either fail pure . smpDecode . encodeUtf8
|
|
|
|
checkEntity :: forall t e e'. (NtfEntityI e, NtfEntityI e') => t e' -> Either String (t e)
|
|
checkEntity c = case testEquality (sNtfEntity @e) (sNtfEntity @e') of
|
|
Just Refl -> Right c
|
|
Nothing -> Left "bad command party"
|
|
|
|
checkEntity' :: forall t p p'. (NtfEntityI p, NtfEntityI p') => t p' -> Maybe (t p)
|
|
checkEntity' c = case testEquality (sNtfEntity @p) (sNtfEntity @p') of
|
|
Just Refl -> Just c
|
|
_ -> Nothing
|